最近在重构代码,思路和制作过程都有不少变化,为了避免混淆,我将之前的博客内容移到了我的有道云笔记里,通过链接可以查看历史文章。
旧版:http://note.youdao.com/noteshare?id=cecdb96a62a65e1819757cfe0aed9715&sub=B79BA533370D49CF97557981AE60F40D
先上一个效果图:
重构版贪吃蛇源代码网盘地址:https://pan.baidu.com/s/18qII8kVQv3PXn-4xpXE6Kw 提取码:s48p
由于之前设想的需求偏多,所以为了支持更多功能和防bug,加入了不少代码。
(1)需要提供加速、减速的功能
(2)需要提供暂停、继续的功能
(3)需要提供DIY关卡的功能
(4)需要提供过关、跳关的功能
(5)不能出现预料之外的bug,比如自己直接向后移动吃自己导致游戏结束
(6)最好能实现音效、音乐
(1)基本组成元素:草地,蛋,蛇,墙
(2)蛇移动过程中,如果吃到蛋自身增长并生成新的蛋。
(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,经过这两个步骤,就完成了一次向右的移动;
如果是蛋,那就记录下所有空地位置,并生成一个新的蛋就好啦;
如果是其他元素(墙或者自己),游戏结束。
理解了上面的核心思路,编码就没啥好说的了,主要是解耦的问题,逻辑类只处理逻辑,视图类只进行绘画。
直接上代码:
移动方向的枚举类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();
}
}
具体细节都加了代码注释,如果有不清楚的,可以评论提出来。。。