Java游戏开发——贪吃蛇

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

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

先上一个效果图:

重构版贪吃蛇源代码网盘地址: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):

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

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

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

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

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

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

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

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<Integer> 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();
	}

}

 

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

  • 41
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
### 回答1: 这里提供一个基于Java Swing开发的简单贪吃游戏代码: ``` import java.awt.Color; import java.awt.Graphics; import java.awt.Point; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.LinkedList; import java.util.Random; import javax.swing.JFrame; import javax.swing.JPanel; public class SnakeGame extends JPanel implements Runnable, KeyListener { private static final long serialVersionUID = 1L; // 游戏宽度 private static final int WIDTH = 500; // 游戏高度 private static final int HEIGHT = 500; // 方格大小 private static final int BLOCK_SIZE = 20; // 的长度 private int snakeLength = 3; // 的坐标 private LinkedList<Point> snake = new LinkedList<Point>(); // 的方向 private int direction = KeyEvent.VK_RIGHT; // 食物坐标 private Point food = new Point(); // 随机种子 private Random random = new Random(); // 游戏是否结束 private boolean gameOver = false; public SnakeGame() { // 初始化界面 JFrame frame = new JFrame("贪吃大作战"); frame.setSize(WIDTH, HEIGHT); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); frame.setResizable(false); frame.addKeyListener(this); frame.add(this); frame.setVisible(true); // 初始化 snake.add(new Point(2, 0)); snake.add(new Point(1, 0)); snake.add(new Point(0, 0)); // 初始化食物 generateFood(); // 启动游戏线程 Thread thread = new Thread(this); thread.start(); } @Override public void paint(Graphics g) { // 清空画布 g.clearRect(0, 0, WIDTH, HEIGHT); // 画 g.setColor(Color.GREEN); for (Point p : snake) { g.fillRect(p.x * BLOCK_SIZE, p.y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); } // 画食物 g.setColor(Color.RED); g.fillRect(food.x * BLOCK_SIZE, food.y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE); // 画分数 g.setColor(Color.BLACK); g.drawString("分数:" + (snakeLength - 3), 10, 20); // 画游戏结束提示 if (gameOver) { g.setColor(Color.RED); g.drawString("游戏结束!", WIDTH / 2 - 30, HEIGHT / 2); } } @Override public void run() { while (!gameOver) { try { // 控制游戏速度 Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } // 移动 moveSnake(); // 判断是否吃到食物 if (snake.getFirst().equals(food)) { snakeLength++; generateFood(); } // 判断是否撞到墙或自己 if (snake.getFirst().x < 0 || snake.getFirst().x >= WIDTH / BLOCK_SIZE || snake.getFirst().y < 0 || snake.getFirst().y >= HEIGHT / BLOCK_SIZE || snake.contains(snake.getFirst())) { gameOver = true; } // 重绘界面 repaint(); } } @Override public void keyPressed(KeyEvent e) { // 更新的方向 if (e.getKeyCode() == KeyEvent.VK_UP && direction != KeyEvent.VK_DOWN) { direction = KeyEvent.VK_UP; } else if (e.getKeyCode() == KeyEvent.VK_DOWN && direction != KeyEvent.VK_UP) { direction = KeyEvent.VK_DOWN; } else if (e.getKeyCode() == KeyEvent.VK_LEFT && direction != KeyEvent.VK_RIGHT) { direction = KeyEvent.VK_LEFT; } else if (e.getKeyCode() == KeyEvent.VK_RIGHT && direction != KeyEvent.VK_LEFT) { direction = KeyEvent.VK_RIGHT; } } @Override public void keyTyped(KeyEvent e) { } @Override public void keyReleased(KeyEvent e) { } // 移动 private void moveSnake() { int x = snake.getFirst().x; int y = snake.getFirst().y; switch (direction) { case KeyEvent.VK_UP: y--; break; case KeyEvent.VK_DOWN: y++; break; case KeyEvent.VK_LEFT: x--; break; case KeyEvent.VK_RIGHT: x++; break; } snake.addFirst(new Point(x, y)); if (snake.size() > snakeLength) { snake.removeLast(); } } // 生成食物 private void generateFood() { while (true) { int x = random.nextInt(WIDTH / BLOCK_SIZE); int y = random.nextInt(HEIGHT / BLOCK_SIZE); food.setLocation(x, y); if (!snake.contains(food)) { break; } } } public static void main(String[] args) { new SnakeGame(); } } ``` 以上代码仅提供参考,可以根据自己的需求进行修改和完善。 ### 回答2: Java贪吃大作战代码是一段用Java语言编写的程序,实现了经典游戏贪吃大作战”。 首先,程序需要定义的属性,比如的位置、尺寸、速度以及方向等信息。通过一个数组或者链表来保存的身体坐标,每个节点表示身体的一个部分。还需要定义食物的属性,比如食物的位置和大小。 接下来,程序需要监听玩家的输入,根据玩家的操作来改变的方向:向上、向下、向左或向右移动。可以使用键盘事件监听,通过按键的keyCode来确定操作。 然后,程序需要实现的移动逻辑。向前移动时,身体的每一节都要跟着移动,并且新增加的食物会被吃掉,使得的身体变长。当的头部与身体相撞或者撞墙时,游戏结束。 在游戏进行时,程序需要不断更新屏幕上的和食物的位置,以及判断是否吃到食物,如果吃到了则生成新的食物。可以使用定时器或者循环来实现游戏的更新和渲染。 最后,当游戏结束时,可以显示游戏结束的提示信息,比如游戏得分、最高分等。 总结来说,Java贪吃大作战代码是通过定义和食物的属性,监听玩家输入,实现的移动逻辑,并在游戏进行时不断更新屏幕上的内容,最后展示游戏结束信息的一段Java程序。编写这样的代码可以锻炼编程能力,并且可以在游戏中体验到程序设计的乐趣。 ### 回答3: 在Java贪吃大作战代码中,首先需要创建一个贪吃的类。这个类包含贪吃的身体坐标、移动方向和长度等属性。在游戏开始时,需要初始化贪吃的位置和长度。 然后,我们需要实现游戏的主循环。在每一轮循环中,根据用户输入或者预设的策略改变贪吃的移动方向,并且根据贪吃当前位置和移动方向计算下一步的位置。如果下一步的位置是食物的位置,那么贪吃的长度加一,并且在地图上生成一个新的食物。如果下一步的位置是地图边界或者贪吃自身的位置,那么游戏结束。 游戏的地图可以使用二维数组来表示,每个元素的值表示该位置的状态,比如空白、贪吃的身体或者食物。 为了让游戏有更好的可玩性,我们可以添加一些额外的功能。比如,在地图上随机生成障碍物,让贪吃在遇到障碍物时无法通过。我们还可以设计多个关卡和不同的游戏模式,让玩家可以选择不同的难度或者挑战各种挑战。此外,我们还可以添加一些特殊道具,比如加速、减速或者变换方向等,让游戏更加有趣。 最后,我们需要实现一些图形界面交互,比如显示地图和贪吃,监听键盘事件来改变贪吃的移动方向。如果有条件的话,我们还可以将游戏进行网络化,实现多人联机对战,让玩家可以与其他玩家进行对战和竞争。 总之,Java贪吃大作战代码需要实现贪吃的移动、长度增加、判断游戏结束等基本功能,并可以通过添加障碍物、地图关卡、道具等来增加游戏的可玩性和趣味性。同时,还可以实现图形界面和网络对战等功能来提供更好的游戏体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值