Java游戏开发——贪吃蛇

最近在重构代码,思路和制作过程都有不少变化,为了避免混淆,我将之前的博客内容移到了我的有道云笔记里,通过链接可以查看历史文章。

旧版:http://note.youdao.com/noteshare?id=cecdb96a62a65e1819757cfe0aed9715&sub=B79BA533370D49CF97557981AE60F40D

先上一个效果图:

Java游戏开发——贪吃蛇_第1张图片

重构版贪吃蛇源代码网盘地址:https://pan.baidu.com/s/18qII8kVQv3PXn-4xpXE6Kw 提取码:s48p

由于之前设想的需求偏多,所以为了支持更多功能和防bug,加入了不少代码。

开发过程

1.分析需求

(1)需要提供加速、减速的功能

(2)需要提供暂停、继续的功能

(3)需要提供DIY关卡的功能

(4)需要提供过关、跳关的功能

(5)不能出现预料之外的bug,比如自己直接向后移动吃自己导致游戏结束

(6)最好能实现音效、音乐

2.分析游戏要素

(1)基本组成元素:草地,蛋,蛇,墙

(2)蛇移动过程中,如果吃到蛋自身增长并生成新的蛋。

(3)蛇吃到自己或者撞上墙游戏结束

3.针对需求和要素进行粗略设计

需求(1):通过控制子线程的休眠时长来控制蛇的移动速度

需求(2):通过设置线程标记量,暂停或继续负责蛇移动的子线程的运行

需求(3):通过一个三维数组来存储多个关卡,每个关卡的地图数组都是由一个二维数组构成。通过设置数组的元素值可以进行DIY

需求(4):通过空地数量,动态生成过关分数,到达分数后,进入下一关。跳关则是通过按键直接执行过关操作

需求(5):记录当前移动方向,禁止向后移动。记录最近一次移动方向,因为在速度较慢时,可能会通过组合键导致蛇最终还是向后吃自己,这是需要注意规避的点

需求(6):搬运推箱子的音乐类代码,可以自行扩展非midi音效

要素(1):草地值为0,蛋为-1,墙为-2,大于0的值表示蛇头或蛇尾。

要素(2)(3):

合并起来叙述,就是一个移动的处理。这里需要记录蛇头的行列数,举例说下。

Java游戏开发——贪吃蛇_第2张图片

现在蛇头为于第3行第2列(行列数从0开始),蛇身长度是1。

我默认它初始移动方向是右,游戏启动后,假设子线程500毫秒执行一次循环,在循环体中,先遍历整个地图数组,将>0的元素值全部加1,此时地图示意图如下:

Java游戏开发——贪吃蛇_第3张图片

因为移动方向是右,所以我们针对蛇头位置右一格进行判断,

如果是空地,直接赋值为1,再遍历一遍数组,发现哪个数值大于蛇长,改为0,经过这两个步骤,就完成了一次向右的移动;

Java游戏开发——贪吃蛇_第4张图片

如果是蛋,那就记录下所有空地位置,并生成一个新的蛋就好啦;

如果是其他元素(墙或者自己),游戏结束。

4.编码实现

理解了上面的核心思路,编码就没啥好说的了,主要是解耦的问题,逻辑类只处理逻辑,视图类只进行绘画。

直接上代码:

移动方向的枚举类DirectionType

package 贪吃蛇重构版;

/**
 * @author 墨染秦月
 * @date 2020年12月31日
 *
 * 描述:移动方向的枚举类
 */
public enum DirectionType{
	UP,DOWN,LEFT,RIGHT
}

逻辑处理类接口

package 贪吃蛇重构版;

/**
 * @author 墨染秦月
 * @date 2020年12月31日
 *
 * 描述:逻辑类抽象接口
 */
public interface IGameLogicInterface {

	//主角上移
	void moveUp();

	//主角下移
	void moveDown();

	//主角左移
	void moveLeft();

	//主角右移
	void moveRight();
	
	//改变移动方向
	void changeDirection(DirectionType direction);
	
	//随机生成一个蛋
	void randomProduceEgg();
	
	//更新蛇尾位置
	void refreshSnakeTail();
	
}

音乐工具类

/**
 * 
 */
package 贪吃蛇重构版;

import java.io.File;

import javax.sound.midi.MidiSystem;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;

/**
 * @author 墨染秦月
 * @date 2020年12月31日
 *
 * 描述:游戏音乐工具类,播放背景音乐
 */
public class GameMusicUtil {
	
	private static String musicFile;
	private static Sequence seq;
	private static Sequencer midi;

	static {
		try {
			musicFile = new String("res/nor.mid");
			seq = MidiSystem.getSequence(new File(musicFile));
			midi = MidiSystem.getSequencer();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void play() {
		try {
			if(midi!=null){
				midi.open();
				midi.setSequence(seq);				
				midi.setLoopCount(Sequencer.LOOP_CONTINUOUSLY);
				midi.start();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	
	
	public static void stop() {
		if(midi!=null){
			midi.stop();
			midi.close();			
		}
	}
	
}

地图工具类,存储各关关卡,注意深拷贝问题

/**
 * 
 */
package 贪吃蛇重构版;

/**
 * @author 墨染秦月
 * @date 2020年12月31日
 *
 * 描述:游戏地图类,存储每个关卡的地图数据
 */
public class GameMapSet {

	// 存放各个游戏关卡的地图数组
	private static int map[][][] = {
			// 第一关
			{ { -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 }, { -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 },
					{ -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 }, { -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 },
					{ -2, 0, 0, 1, 0, 0, -1, 0, 0, -2 }, { -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 },
					{ -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 }, { -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 },
					{ -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 }, { -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 } },
			// 第二关,中间插一个T形砖块
			{ { -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 }, { -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 },
					{ -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 }, { -2, 0, 0, 1, 0, 0, 0, 0, 0, -2 },
					{ -2, 0, 0, -2, -2, -2, -2, -2, 0, -2 }, { -2, 0, 0, 0, 0, -2, 0, 0, 0, -2 },
					{ -2, 0, 0, 0, 0, -2, 0, 0, 0, -2 }, { -2, 0, -1, 0, 0, 0, 0, 0, 0, -2 },
					{ -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 }, { -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 } },
			// 第三关,中间放一个围了三面墙的迷宫
			{ { -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 }, { -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 },
					{ -2, 0, -2, -2, -2, -2, -2, -2, 0, -2 }, { -2, 0, 0, 0, 0, 0, 0, -2, 0, -2 },
					{ -2, 0, 1, 0, 0, -1, 0, -2, 0, -2 }, { -2, 0, 0, 0, 0, 0, 0, -2, 0, -2 },
					{ -2, 0, 0, 0, 0, 0, 0, -2, 0, -2 }, { -2, 0, -2, -2, -2, -2, -2, -2, 0, -2 },
					{ -2, 0, 0, 0, 0, 0, 0, 0, 0, -2 }, { -2, -2, -2, -2, -2, -2, -2, -2, -2, -2 } },

	};

	// 游戏关卡数目
	private static int count = map.length;

	// 返回指定关卡的地图数据克隆数组
	public static int[][] getMap(int grade) {
		if (grade >= 0 && grade < count) {
			return realCloneArray(map[grade]);
		}
		return realCloneArray(map[0]);
	}

	// 深拷贝二维数组
	private static int[][] realCloneArray(int[][] map) {
		int[][] cloneMap = new int[map.length][map[0].length];
		for (int i = 0; i < map.length; i++) {
			cloneMap[i] = map[i].clone();
		}
		return cloneMap;
	}

	// 获取关卡数量
	public static int getGradeCount() {
		return count;
	}

}

游戏面板类,主要负责监听键盘事件,显示界面元素,核心逻辑都交给逻辑类做处理

package 贪吃蛇重构版;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;


import javax.swing.JOptionPane;
import javax.swing.JPanel;

/**
 * @author 墨染秦月
 * @date 2020年12月31日
 *
 *       描述:游戏面板类,只负责界面的显示。。。 元素手撕,不使用其他图片素材
 */
public class GamePanel extends JPanel implements KeyListener {

	private int width, height, marginLeft, marginTop;
	private GameLogic mLogic;
	private int grade;
	private static final int WALL = -2,EGG = -1, GRASS = 0;
	private int[][] map;
	private boolean isAcceptKey;

	public GamePanel() {
		setSize(600, 600);
		// 屏幕宽度
		this.width = getWidth();
		// 屏幕高度
		this.height = getHeight();
		// 游戏逻辑类实例化
		mLogic = GameLogic.getInstance();
		//观察者模式注册回调,监听游戏结束,失败,请求绘画的事件
		mLogic.registerClient(this);
		// 初始化关卡
		initGame(grade);
		setFocusable(true);
		addKeyListener(this);
	}

	private void initGame(int grade) {
		// 设置关卡
		mLogic.setGrade(grade);
		map = mLogic.getMapData();
		// 左上角的左侧外边距
		marginLeft = (width - map[0].length * 30) / 2;
		// 左上角的上侧外边距
		marginTop = (height - map.length * 30) / 2;
		//初始化接受按键事件
		isAcceptKey = true;
		repaint();
		mLogic.play();
	}

	public void paint(Graphics g) {
		// 清空画布
		g.setColor(Color.white);
		g.fillRect(0, 0, width, height);
		// 绘制游戏元素
		for (int i = 0; i < map.length; i++)
			for (int j = 0; j < map[0].length; j++) {
				switch (map[i][j]) {
				case WALL:
					//墙画成填充粉色,两条白色斜条纹的砖块
					g.setColor(Color.PINK);
					g.fillRect(marginLeft + j * 30, marginTop + i * 30, 30, 30);
					g.setColor(Color.WHITE);
					g.drawLine(marginLeft+j*30 , marginTop + i * 30, marginLeft + (j+1) * 30, marginTop + i * 30+10);
					g.drawLine(marginLeft+j*30 , marginTop + i * 30+10, marginLeft + (j+1) * 30, marginTop + i * 30+20);
					g.drawLine(marginLeft+j*30 , marginTop + i * 30+20, marginLeft + (j+1)* 30, marginTop + i * 30+30);
					break;
				case EGG:
					//先用黄砖顶包,后面优化
					g.setColor(Color.YELLOW);
					g.fillRect(marginLeft + j * 30, marginTop + i * 30, 30, 30);
					break;
				case GRASS:
					//草地当然是绿色。。。
					g.setColor(Color.GREEN);
					g.fillRect(marginLeft + j * 30, marginTop + i * 30, 30, 30);
					break;
				default:
					//奇数黑块,偶数白块,画成黑白相间的玩意,眼睛后面再画
					if(map[i][j]%2==1){
						g.setColor(Color.BLACK);
					}else{
						g.setColor(Color.WHITE);						
					}
					g.fillRect(marginLeft + j * 30, marginTop + i * 30, 30, 30);
					break;
				}

			}
		g.setColor(Color.RED);
		g.setFont(new Font("楷体_2312", Font.BOLD, 30));
		g.drawString("现在是第  "+String.valueOf(grade + 1)+" 关     ", 150, 140);
		g.drawString(mLogic.getNowScore()+"/"+mLogic.getFinishScore(), 370, 140);

	}

	
	
	@Override
	public void keyPressed(KeyEvent e) {
		//弹框过程中,不处理任何按键事件
		if(!isAcceptKey){
			return ;
		}
		if (e.getKeyCode() == KeyEvent.VK_UP) {
			mLogic.changeDirection(DirectionType.UP);
		}
		if (e.getKeyCode() == KeyEvent.VK_DOWN) {
			mLogic.changeDirection(DirectionType.DOWN);
		}
		if (e.getKeyCode() == KeyEvent.VK_LEFT) {
			mLogic.changeDirection(DirectionType.LEFT);
		}
		if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
			mLogic.changeDirection(DirectionType.RIGHT);
		}

		if (e.getKeyCode() == KeyEvent.VK_Q) {//减速
			mLogic.pleaseMovingSlower();
		}
		if (e.getKeyCode() == KeyEvent.VK_E) {//加速
			mLogic.pleaseMovingQuicker();
		}
		
		if (e.getKeyCode() == KeyEvent.VK_A) {//上一关
			isAcceptKey = true;
			initGame(grade > 0 ? --grade : 0);
		}
		if (e.getKeyCode() == KeyEvent.VK_D) {//下一关
			isAcceptKey = true;
		    initGame(grade < GameMapSet.getGradeCount() - 1 ? ++grade : GameMapSet.getGradeCount() - 1);
		}
		if (e.getKeyCode() == KeyEvent.VK_W) {// 暂停游戏
			mLogic.pause();
		}
		if (e.getKeyCode() == KeyEvent.VK_S) {// 继续游戏
			mLogic.play();
		}

	}
	
	public void notifyGameOver(){
		isAcceptKey = false;
		String msg = "游戏结束,是否重来?";
		int type = JOptionPane.YES_NO_OPTION;
		String title = "提示";
		int choice = 0;
		choice = JOptionPane.showConfirmDialog(this, msg, title, type);
		if (choice == 1) {
			System.exit(0);
		} else {
			isAcceptKey = true;
			initGame(grade);
		}
		
	}
	
	public void notifyGameFinished(){
		isAcceptKey = false;
		if (grade == GameMapSet.getGradeCount()-1) {
			displayOkToast("恭喜通过最后一关");
			System.exit(0);
		} else {
			String msg = "恭喜你通过第" + (grade + 1) + "关!!!\n是否要进入下一关?";
			int type = JOptionPane.YES_NO_OPTION;
			String title = "过关";
			int choice = 0;
			choice = JOptionPane.showConfirmDialog(this, msg, title, type);
			// 询问是否进入下一关
			if (choice == 1) {
				System.exit(0);
			} else {
				isAcceptKey = true;
				initGame(++grade);
			}
		}
	}
	
	public void notifyGamePaint(){
		map = mLogic.getMapData();
		repaint();
	}
	
	public void displayOkToast(String str) {
		JOptionPane.showMessageDialog(this, str, "提示", JOptionPane.YES_NO_CANCEL_OPTION);
	}

	public void displayErrorToast(String str) {
		JOptionPane.showMessageDialog(this, str, "提示", JOptionPane.ERROR_MESSAGE);
	}


	@Override
	public void keyTyped(KeyEvent e) {

	}

	@Override
	public void keyReleased(KeyEvent e) {

	}

}

游戏逻辑类,处理核心逻辑,提供数据给面板类做显示。这里用到了观察者模式,将面板注册在了逻辑类中,游戏结束后进行通知。

/**
 * 
 */
package 贪吃蛇重构版;

import java.util.LinkedList;
import java.util.Random;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

/**
 * @author 墨染秦月
 * @date 2020年12月31日
 *
 *       描述:游戏核心逻辑处理
 * 
 *       游戏核心逻辑: 贪吃蛇的移动涉及速度,方向,吃蛋后自身增长,碰壁,蛇头碰到蛇身五大元素
 *       (1)移动速度,可以通过设置子线程的休眠时间来控制移动速度,休眠时间越短,移动速度越快
 *       (2)方向,贪吃蛇的移动方向由玩家控制,需要注意的是,蛇头不能向自己当前移动方向的反方向进行过变更
 *       (3)吃蛋后自身增长,我的想法是,将所以空地状态初始化为0,蛇头初始化为1,蛋初始化为-1,障碍物初始化为-2 
 *       一个4*4的初始地图示意图:
 *       -2 -2 -2 -2 -2 -2 
 *       -2  0  0  0  0 -2 
 *       -2  0  0  0  0 -2 
 *       -2  0  1  0  0 -2 
 *       -2  0  0  0 -1 -2 
 *       -2 -2 -2 -2 -2 -2 
 *       此时,蛇身长为1,位于第4行第3列,蛋位于第5行第5列。
 *       记录下蛇头的数组下标snakeHeadRow= 3,snakeHeadColumn = 2,snakeLength = 1;
 * 
 *       记录主角的初始移动方向为右,游戏启动后,假设子线程500毫秒执行一次循环,在循环体中,先遍历整个地图数组,将>0的元素值全部加1,此时地图示意图如下:
 * 
 *       -2 -2 -2 -2 -2 -2 
 *       -2  0  0  0  0 -2 
 *       -2  0  0  0  0 -2 
 *       -2  0  2  0  0 -2 
 *       -2  0  0  0 -1 -2 
 *       -2 -2 -2 -2 -2 -2
 * 
 *       根据移动方向(右)和map[snakeHeadRow][snakeHeadColumn+1]的内容做相应处理
 *       switch(map[snakeHeadRow][snakeHeadColumn+1]){ 
 *       //右移碰到墙壁 case -2 :
 *       GameOver(); 
 *       break; 
 *       //右移吃蛋,自身加1 
 *       case -1 : 
 *       snakeLength++;
 *       map[snakeHeadRow][snakeHeadColumn+1] = 1; 
 *       randomProduceTheEgg(); 
 *       break;
 *       //右移到空地 : 
 *       case 0 : 
 *       map[snakeHeadRow][snakeHeadColumn+1] = 1;
 *       //更新蛇尾位置,遍历地图数据将数组元素数值超过snakeLength的值赋值为0,变成空地 
 *       refreshSnakeTail();
 *       break; 
 *       //右移撞到自身 
 *       default : 
 *       GameOver(); 
 *       break; }
 * 
 *       (4)(5)都包含在上面了,到这里,游戏的核心逻辑就讲述完成了。。。
 * 
 *       非核心逻辑: (1)音效播放。。。。可以通过GameMusicUtil来进行
 *       (2)关卡丰富化,关卡地图数据存储到GameMapSet中,每个关卡设置一个分数,关卡到了这个分数,进入下一关 (3)提供游戏暂停的功能
 *       (4)通过按键增加主角移动速度
 * 
 */
public class GameLogic implements IGameLogicInterface {

	// 单例对象
	private static volatile GameLogic instance;
	// 地图数组
	private int[][] map;
	// 游戏元素常量
	private static final int WALL = -2, EGG = -1, GRASS = 0, HEAD = 1;
	// 主角所在行列数
	private int snakeHeadRow, snakeHeadColumn;
	// 主角移动方向
	private volatile DirectionType direction,previorDirection;
	private volatile int speed;
	private int nowScore, finishScore;
	private int snakeLength;
	private volatile GameThread gameThread;
	private GamePanel gamePanel;

	{
		gameThread = new GameThread();
		gameThread.start();
	}
	
	// 单例模式做游戏逻辑处理类
	public static GameLogic getInstance() {
		if (instance == null) {
			synchronized (GameLogic.class) {
				if (instance == null) {
					instance = new GameLogic();					
				}
			}
		}
		return instance;
	}

	// 设置关卡,初始化地图数据,初始化行列数和主角位置
	public void setGrade(int grade) {
		initMap(grade);
		initParams();
	}

	// 初始化地图数据
	private void initMap(int grade) {
		map = GameMapSet.getMap(grade);
	}

	// 初始化参数
	private void initParams() {
		// 初始化移动方向向右
		direction = DirectionType.RIGHT;
		previorDirection = DirectionType.RIGHT;
		// 统计角色可移动位置数目
		int count = 0;
		// 获取snakeHead在第几行第几列
		for (int i = 0; i < map.length; i++) {
			for (int j = 0; j < map[0].length; j++) {
				switch (map[i][j]) {
				case HEAD:
					snakeHeadRow = i;
					snakeHeadColumn = j;
					break;
				case EGG:
				case GRASS:
					count++;
					break;
				}
			}
		}
		// 速度初始化
		speed = 1;
		// 长度初始化
		snakeLength = 1;
		// 当前关卡分数初始化为0
		nowScore = 0;
		// 设定当前关卡的过关分数 = 可移动位置数目/4
		finishScore = count / 4;
	}

	// 观察者模式注册监听,等游戏结束后,进行回调
	public void registerClient(GamePanel gamePanel) {
		this.gamePanel = gamePanel;
	}

	// 开始游戏/继续游戏
	public void play() {
		gameThread.play();
	}

	// 暂停游戏
	public void pause() {
		gameThread.pause();
	}

	// 返回地图数据
	public int[][] getMapData() {
		return realCloneArray(map);
	}

	// 深拷贝二维数组
	private int[][] realCloneArray(int[][] map) {
		int[][] cloneMap = new int[map.length][map[0].length];
		for (int i = 0; i < map.length; i++) {
			cloneMap[i] = map[i].clone();
		}
		return cloneMap;
	}

	// 超出蛇长度的值,肯定是移动前最末端的那个位置,将其替换成GRASS
	@Override
	public void refreshSnakeTail() {
		for (int i = 0; i < map.length; i++) {
			for (int j = 0; j < map[0].length; j++) {
				if (map[i][j] > snakeLength) {
					map[i][j] = GRASS;
				}
			}
		}
	}

	// 改变主角方向
	@Override
	public void changeDirection(DirectionType direction) {
		this.direction = direction;
	}

	// 每次加分后,检查玩家是否过关
	private boolean checkGameFinished() {
		if (nowScore >= finishScore) {
			gamePanel.notifyGameFinished();
			return true;
		}
		return false;
	}

	@Override
	public void moveUp() {
		switch (map[snakeHeadRow - 1][snakeHeadColumn]) {
		// 移动到草地
		case GRASS:
			map[--snakeHeadRow][snakeHeadColumn] = HEAD;
			refreshSnakeTail();
			break;
		// 吃蛋
		case EGG:
			map[--snakeHeadRow][snakeHeadColumn] = HEAD;
			snakeLength++;
			nowScore++;
			if(!checkGameFinished()){
				randomProduceEgg();	
			}
			break;
		// 吃到自己或者撞上墙
		default:
			// 游戏结束
			gameThread.pause();
			gamePanel.notifyGameOver();
			break;
		}
	}

	@Override
	public void moveDown() {
		switch (map[snakeHeadRow + 1][snakeHeadColumn]) {
		// 移动到草地
		case GRASS:
			map[++snakeHeadRow][snakeHeadColumn] = HEAD;
			refreshSnakeTail();
			break;
		// 吃蛋
		case EGG:
			map[++snakeHeadRow][snakeHeadColumn] = HEAD;
			snakeLength++;
			nowScore++;
			if(!checkGameFinished()){
				randomProduceEgg();				
			}
			break;
		// 吃到自己或者撞上墙
		default:
			// 游戏结束
			gameThread.pause();
			gamePanel.notifyGameOver();
			break;
		}
	}

	@Override
	public void moveLeft() {
		switch (map[snakeHeadRow][snakeHeadColumn - 1]) {
		// 移动到草地
		case GRASS:
			map[snakeHeadRow][--snakeHeadColumn] = HEAD;
			refreshSnakeTail();
			break;
		// 吃蛋
		case EGG:
			map[snakeHeadRow][--snakeHeadColumn] = HEAD;
			snakeLength++;
			nowScore++;
			if(!checkGameFinished()){
				randomProduceEgg();				
			}
			break;
		// 吃到自己或者撞上墙
		default:
			// 游戏结束
			gameThread.pause();
			gamePanel.notifyGameOver();
			break;
		}
	}

	@Override
	public void moveRight() {
		switch (map[snakeHeadRow][snakeHeadColumn + 1]) {
		// 移动到草地
		case GRASS:
			map[snakeHeadRow][++snakeHeadColumn] = HEAD;
			refreshSnakeTail();
			break;
		// 吃蛋
		case EGG:
			map[snakeHeadRow][++snakeHeadColumn] = HEAD;
			snakeLength++;
			nowScore++;
			if(!checkGameFinished()){
				randomProduceEgg();				
			}
			break;
		// 吃到自己或者撞上墙
		default:
			// 游戏结束,需要更新子线程。。。
			gameThread.pause();
			gamePanel.notifyGameOver();
			break;
		}
	}
	
	public int getNowScore(){
		return nowScore;
	}
	
	public int getFinishScore(){
		return finishScore;
	}

	//隔半秒检测一次,如果当前方向向右,突然在半秒内按到上和左的组合键,还是会咬到自己。。。。、
	//做了异常处理
	private void move() {
		// 如果蛇长度>=2,最近一次移动方向是上,假设当前要移动的方向是上或者下,这将没有任何意义,直接恢复成上一次移动方向
		// (1)因为当前方向未改变
		// (2)因为蛇不能向后移动直接撞自己
		if (snakeLength >= 2) {
			switch (this.direction) {
			case UP:
			case DOWN:
				if (previorDirection == DirectionType.DOWN || previorDirection == DirectionType.UP) {
//					System.out.println(this.direction+"无效的方向"+direction);
					direction = previorDirection;
				}
				break;
			case LEFT:
			case RIGHT:
				if (previorDirection == DirectionType.LEFT || previorDirection == DirectionType.RIGHT) {
//					System.out.println(this.direction+"无效的方向"+direction);
					direction = previorDirection;
				}
				break;
			}
		}
		//大于0的全部地图元素加1
		doSomeThing();
		switch (direction) {
		case UP:
			moveUp();
			break;
		case DOWN:
			moveDown();
			break;
		case LEFT:
			moveLeft();
			break;
		case RIGHT:
			moveRight();
			break;
		}
		gamePanel.notifyGamePaint();
		//记录最近一次移动方向。。。
		previorDirection = direction;
		printMapDetails();
	}

	
	// 加速
	public void pleaseMovingQuicker() {
		if (speed < 5) {
			speed++;
		}
	}

	// 减速
	public void pleaseMovingSlower() {
		if (speed > 1) {
			speed--;
		}
	}

	// 将大于0的元素全部加1
	private void doSomeThing() {
		for (int i = 0; i < map.length; i++) {
			for (int j = 0; j < map[0].length; j++) {
				if (map[i][j] > 0) {
					map[i][j]++;
				}
			}
		}
	}

	// 随机生成蛋
	@Override
	public void randomProduceEgg() {
		// 将草地元素全部收集到list,然后随机取一个变成蛋
		LinkedList list = new LinkedList<>();
		for (int i = 0; i < map.length; i++) {
			for (int j = 0; j < map[0].length; j++) {
				if (map[i][j] == GRASS) {
					// 行数i*总列数+列数得到一个值
					list.add(i * map[0].length + j);
				}
			}
		}
		Random random = new Random();
		int index = random.nextInt(list.size());
		int row = list.get(index) / map[0].length;
		int column = list.get(index) % map[0].length;
		map[row][column] = EGG;
	}

	// 定期执行角色移动的子线程
	class GameThread extends Thread {

		private volatile boolean gameThreadAliveFlag = true;
		//初始化为false,游戏开始后,改成true
		private volatile boolean gamePlayingFlag = false;

		public GameThread() {
		}

		public void stopThread() {
			pause();
			gameThreadAliveFlag = false;
		}

		public void pause() {
			gamePlayingFlag = false;
		}

		public void play() {
			gamePlayingFlag = true;
		}

		@Override
		public void run() {
			super.run();
			while (gameThreadAliveFlag) {
				while (gamePlayingFlag) {
					try {
						// 隔半秒移动一次,可以通过speed控制速度
						Thread.sleep(500 - (speed - 1) * 100);
						move();
					} catch (InterruptedException e) {
						e.printStackTrace();
					}				}
				try {
					// 隔100ms检查下游戏的状态是不是在运行
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}

		}
	}

	// 打印地图细节,用于测试
	private void printMapDetails() {
		// @表示snake,$表示蛋,#表示草地,&表示砖块
		System.out.println("***************************************");
		for (int i = 0; i < map.length; i++) {
			for (int j = 0; j < map[0].length; j++) {
				switch (map[i][j]) {
				case GRASS:
					System.out.print("#");
					break;
				case EGG:
					System.out.print("$");
					break;
				case WALL:
					System.out.print("&");
					break;
				default:
					System.out.print("@");
					break;
				}
			}
			System.out.println();
		}
		System.out.println("***************************************");
	}

}

窗口类,显示游戏面板:

package 贪吃蛇重构版;

import java.awt.Color;

import javax.swing.JFrame;
import javax.swing.JOptionPane;

/**
 * @author 墨染秦月
 * @date 2020年12月31日
 *
 * 描述:窗口类
 * 上下左右键移动,A上一关,D下一关,W暂停,S继续,Q减速,E加速
 */
public class GameClient extends JFrame{

	public GameClient(){
		//设置窗口标题
		super("贪吃蛇。。。");
		//生成游戏面板对象
		GamePanel gamePanel = new GamePanel();
		gamePanel.setBackground(Color.white);
		//将游戏面板添加到窗口的容器中
		getContentPane().add(gamePanel);
		//点击右上角的x可以退出游戏
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		//设置可见性
		setVisible(true);
		//设置窗口不可动态调整
		setResizable(false);
		//设置窗口大小
		setSize(600,600);
		//设置窗口左上角坐标
		setLocation(300,20);		
		//播放音乐
		GameMusicUtil.play();
	}
	
	public static void main(String[] args) {
		new GameClient();
	}

}

 

具体细节都加了代码注释,如果有不清楚的,可以评论提出来。。。

你可能感兴趣的:(Java小游戏开发合集,贪吃蛇,游戏开发,Swing)