老Java程序员花两天做了个消消乐(天天爱消除)

老Java程序员花两天做了个消消乐(天天爱消除)

引言:

一直就想做一个消消乐,这次正好找到了素材,就自己琢磨写了一个,我觉得这个游戏难点就在消除、以及消除后的下落,其他的地方也就还好,这次做完了写个文章大家唠一波。

效果图

在这里插入图片描述

实现思路

1.绘制窗口、按钮、边框等。
2.实现Card类,用来代表每一个小图形。
3.创建下标集合,因图片下标是0-5,所以用随机函数随机出下标,用来代表不同的图形,并依次添加打集合indexs中。
4.对此集合进行随机排序处理。
5.创建二维数组9行8列,根据集合的下标和二维数组对应的下标实例化各个卡片,并对应放在二维数组中。
6.设定卡片交换—当前卡片的上下左右才能交换。
7.判断横向、纵向是否超过3个相同的,是则消除。
8.消除后对应的卡片下落。

代码实现

创建窗口

首先创建一个游戏窗体类GameFrame,继承至JFrame,用来显示在屏幕上(window的对象),每个游戏都有一个窗口,设置好窗口标题、尺寸、布局等就可以。

/*
 * 游戏窗体类
 */
public class GameFrame extends JFrame {
    
    
	
	public GameFrame() {
    
    
		setTitle("消消乐");//设置标题
		setSize(386, 440);//设定尺寸
		setLayout(new BorderLayout());
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//点击关闭按钮是关闭程序
        setLocationRelativeTo(null);   //设置居中
    	setResizable(false); //不允许修改界面大小
	}
}

创建面板容器GamePanel继承至JPanel

package main;

import java.awt.Graphics;
import javax.swing.JFrame;
import javax.swing.JPanel;
/*
 * 画布类
 */
public class GamePanel extends JPanel{
    
    
	GamePanel gamePanel=this;
	private JFrame mainFrame=null;
	//构造里面初始化相关参数
	public GamePanel(JFrame frame){
    
    
		this.setLayout(null);
		mainFrame = frame;
		
		mainFrame.setVisible(true);
	}
	
	@Override
	public void paint(Graphics g) {
    
    
		
	}
}

再创建一个Main类,来启动这个窗口,用来启动。

public class Main {
    
    
	//主类
	public static void main(String[] args) {
    
    
		GameFrame frame = new GameFrame();
		GamePanel panel = new GamePanel(frame);
		frame.add(panel);
		frame.setVisible(true);//设定显示
	}
}

右键执行这个Main类,窗口建出来了
在这里插入图片描述

创建菜单及菜单选项

创建菜单

//初始化按钮
private void  initMenu(){
    
    
	// 创建菜单及菜单选项
	jmb = new JMenuBar();
	JMenu jm1 = new JMenu("游戏");
	jm1.setFont(new Font("黑体", Font.BOLD, 15));// 设置菜单显示的字体
	JMenu jm2 = new JMenu("帮助");
	jm2.setFont(new Font("黑体", Font.BOLD, 15));// 设置菜单显示的字体
	
	JMenuItem jmi1 = new JMenuItem("开始新游戏");
	JMenuItem jmi2 = new JMenuItem("退出");
	jmi1.setFont(new Font("黑体", Font.BOLD, 15));
	jmi2.setFont(new Font("黑体", Font.BOLD, 15));
	
	JMenuItem jmi3 = new JMenuItem("操作说明");
	jmi3.setFont(new Font("黑体", Font.BOLD, 15));
	JMenuItem jmi4 = new JMenuItem("胜利条件");
	jmi4.setFont(new Font("黑体", Font.BOLD, 15));
	
	jm1.add(jmi1);
	jm1.add(jmi2);
	
	jm2.add(jmi3);
	jm2.add(jmi4);
	
	jmb.add(jm1);
	jmb.add(jm2);
	mainFrame.setJMenuBar(jmb);// 菜单Bar放到JFrame上
	
	jmi1.addActionListener(this);
	jmi1.setActionCommand("Restart");
	jmi2.addActionListener(this);
	jmi2.setActionCommand("Exit");
	
	jmi3.addActionListener(this);
	jmi3.setActionCommand("help");
	jmi4.addActionListener(this);
	jmi4.setActionCommand("win");
}

实现ActionListener并重写方法actionPerformed
在这里插入图片描述
actionPerformed方法的实现

@Override
public void actionPerformed(ActionEvent e) {
    
    
	String command = e.getActionCommand();
	UIManager.put("OptionPane.buttonFont", new FontUIResource(new Font("宋体", Font.ITALIC, 18)));
	UIManager.put("OptionPane.messageFont", new FontUIResource(new Font("宋体", Font.ITALIC, 18)));
	if ("Exit".equals(command)) {
    
    
		Object[] options = {
    
     "确定", "取消" };
		int response = JOptionPane.showOptionDialog(this, "您确认要退出吗", "",
				JOptionPane.YES_OPTION, JOptionPane.QUESTION_MESSAGE, null,
				options, options[0]);
		if (response == 0) {
    
    
			System.exit(0);
		} 
	}else if("Restart".equals(command)){
    
    
		if(!"end".equals(gamePanel.gameFlag)){
    
    
			JOptionPane.showMessageDialog(null, "正在游戏中无法重新开始!",
					"提示!", JOptionPane.INFORMATION_MESSAGE); 
		}else {
    
    
			restart();
		}
	}else if("help".equals(command)){
    
    
		JOptionPane.showMessageDialog(null, "鼠标点击选中后,与相邻的切换,超过3个成行或者成列则消除!",
				"提示!", JOptionPane.INFORMATION_MESSAGE);
	}else if("win".equals(command)){
    
    
		JOptionPane.showMessageDialog(null, "300秒3000分胜利,否则失败!",
				"提示!", JOptionPane.INFORMATION_MESSAGE);
	}
}

在这里插入图片描述

初始化图片

将所有要用到的图片初始化,方便待会使用

public class ImageValue {
    
    
	//小卡片
    public static List<BufferedImage> itemImageList = new ArrayList<BufferedImage>();
    //路径
    public static String ImagePath = "/images/";
    //将图片初始化
    public static void init(){
    
    
    	String path = "";
        //图片初始化
        for(int i=0;i<=5;i++){
    
    
            try {
    
    
            	path = ImagePath +"tile_"+ i+".png";
            	itemImageList.add(ImageIO.read(ImageValue.class.getResource(path)));
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
        
    }
}

初始化下标集合

1.初始化下标值,随机从6张图片中选取,下标[0-5]。
2.8列9行,当集合的长度满足72则跳出while循环。
3.使用 Collections.shuffle 对集合进行随机排序(其实不排序也行,本身也是随机来的)。

//随机排序
private void sortImage() {
    
    
   Collections.shuffle(indexs);
}
//初始化下标值
private void initIndexs() {
    
    
	Random random = new Random();
	int n ;
	while(true){
    
    //
		n = random.nextInt(6);//随机从6张图片下标中选取[0-5]
		indexs.add(n);
		if(indexs.size()==72){
    
    
			break;
		}
	}
}

绘制卡片

drawImage介绍

此例中,因为图片是合在一起的,需要裁剪
在这里插入图片描述
所以要用以下方法:

g.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer)

其中img是Image图片对象,而d开头的都是destinetiong,s开头的是source。

也就是说分别定义好来源和目标的两个矩形的左上角和右下角的点,它会自动帮你剪裁和适应。
在这里插入图片描述


public class Card {
    
    
	private int x = 0;//对应行
	private int y = 0;//对应列
	private int dx = 0;//图形显示左上角x位置
	private int odx = 0;//图形更新后显示左上角x位置
	private int dy = 0;//图形显示左上角y位置
	private int ody = 0;//图形更新后显示左上角y位置
	private int dir = 1;//方向 
	private int width =32;//宽
	private int height = 32;//高
	private int pIndex = 0;//对应素材图片下标
	private int index = 0;//对应图片下标值
	private int type = 1;//1:10张的   2:20张的
	private BufferedImage image = null;//图片对象
	private GamePanel panel=null;//GamePanel
	private boolean alive=true;//是否存活
	private boolean selected = false;//是否选中
	private int moveFlag=0;//移动标示  0 不移动 1 横向移动 2纵向移动
	private int speed=15;//移动速度
	
	public Card(int x,int y,int pIndex,GamePanel panel){
    
    
		this.x=x;
		this.y=y;
		this.dx = 40+y*(32+3)+10;
		this.dy = 35+x*(32+3)+10;
		
		this.panel=panel;
		this.pIndex=pIndex;
		
		this.image = ImageValue.itemImageList.get(pIndex);
  }
  	//绘制
	public void draw(Graphics g) {
    
    
		int index = this.index;
		//index 默认是0,就是从图片中截取第一个
		int sx1 = index*32;
		int sy1 = 0;
		//截取的右下角计算
		int sx2 = (index+1)*32;
		int sy2 = 32;
		
		g.drawImage(this.image,dx, dy,dx+width,dy+height,sx1,sy1,sx2,sy2 ,null );
	}
}

创建一个Card实例,并将它设置给二维数组第一个元素

//初始化卡片
private void initCards() {
    
    
	Card card = new Card(0, 0, 0, this);
	cards[0][0]=card;
}

paint方法绘制一个边框,并把二维数组的card绘制

	@Override
	public void paint(Graphics g) {
    
    
		super.paint(g);
		//绘制边框
		BasicStroke bs_2=new BasicStroke(3,BasicStroke.CAP_ROUND,BasicStroke.JOIN_MITER);
		Graphics2D g_2d=(Graphics2D)g;
		g_2d.setColor(new Color(0,191,255));
		g_2d.setStroke(bs_2);
		g_2d.drawRect(38, 32, 305, 334);

		Card card;
		for (int i = 0; i <ROWS; i++) {
    
    
			for (int j = 0; j < COLS; j++) {
    
    
				card = cards[i][j];
				if(card!=null){
    
    
					card.draw(g);
				}
			}
		}

在这里插入图片描述
在Card类中加入线程,更新index,因为index的变更会更新裁剪位置,此方法要在构造函数中调用。

private void rock() {
    
    
	new Thread(new Runnable() {
    
    
		@Override
		public void run() {
    
    
			while (alive) {
    
    
				try {
    
    
					Thread.sleep(200);
				} catch (InterruptedException e) {
    
    
					e.printStackTrace();
				}
				
				index++;
				if(index==10){
    
    
					index=0;
				}
			}
		}
	}).start();
}

在GamePanel中开启主线程,用来repaint,即可看到动画。

private class RefreshThread implements Runnable {
    
    
	@Override
	public void run() {
    
    
		while (true) {
    
    
			if ("start".equals(gameFlag)) {
    
    
				repaint();
			}
			
			try {
    
    
				Thread.sleep(200);
			} catch (InterruptedException e) {
    
    
			}
		}
	}
}

启动线程

gameFlag="start";
//主线程启动
mainThread = new Thread(new RefreshThread());
mainThread.start();

在这里插入图片描述
将卡片补齐,修改initCards方法

//初始化卡片
private void initCards() {
    
    
	Card card;
	int index = 0 ;
	int temp=0;
	for (int i = 0; i <ROWS; i++) {
    
    
		for (int j = 0; j < COLS; j++) {
    
    
			temp = Integer.valueOf(String.valueOf(indexs.get(index)));
			card = new Card(i, j, temp, this);
			
			cards[i][j]=card;
			index++;
		}
	}
}

此时效果:
在这里插入图片描述

加入点击事件

Card类中加入isPoint方法用来判断鼠标点击是否在范围内。

//判断鼠标是否卡片范围内
boolean isPoint(int x,int y){
    
    
	//大于左上角,小于右下角的坐标则肯定在范围内
	if(x>this.dx && y >this.dy
		&& x<this.dx+this.width && y <this.dy+this.height){
    
    
		return  true;
	}
	return false;
}

GamePanel加入方法判断交换的位置,必须在其上下方、或者左右方才允许交换。(当然如果选择交换的本身是一样的,则同样不允许交换)

//相邻才能交换
private int checkTran(Card card) {
    
    
	if(card.getpIndex()==curCard.getpIndex()){
    
    //相同的不交换
		return 4;
	}
	
	int x = curCard.getX();
	int y = curCard.getY();
	
	int x1 = card.getX();
	int y1 = card.getY();
	
	if(y==y1){
    
    //在上下
		if(x1+1==x||x1-1==x){
    
    
			return 2;
		}
	}
	if(x==x1){
    
    //在左右
		if(y1+1==y||y1-1==y){
    
    
			return 1;
		}
	}
	
	return 0;
}

事件代码,tran的方法先写个空的,等会来写

//鼠标事件的创建
private void createMouseListener() {
    
    
	MouseAdapter mouseAdapter = new MouseAdapter() {
    
    
		
		@Override
		public void mouseClicked(MouseEvent e) {
    
    
			if(!"start".equals(gameFlag)) return ;
			
			int x = e.getX();
			int y = e.getY();
			Card card;
			for (int i = 0; i <ROWS; i++) {
    
    
				for (int j = 0; j < COLS; j++) {
    
    
					card = cards[i][j];
					if(card==null)continue;
					
					if(card.isPoint(x, y)){
    
    
						MusicPlayer.chooseMisic();
						if(curCard==null){
    
    
							curCard = card ;
							card.setSelected(true);
						}else {
    
    
							int dir= checkTran(card);
							if(dir!=0&&dir!=4){
    
    //相邻才能交换
								tran(card,dir);
							}else {
    
    //不是相邻则当前取消选择
								curCard.setSelected(false);
								card.setSelected(true);
								curCard = card ;
							}
						}
						return ;//直接跳出
					}
				}
			}
		}
	};
	addMouseMotionListener(mouseAdapter);
	addMouseListener(mouseAdapter);
}

tran方法实现

1.横向交换—交换其Y值
2.纵向交换—交换其X值
3.交换在2维数组中的对应位置
4.两个卡片均执行move方法

protected void tran(Card card,int dir) {
    
    
	Card tempCard=curCard;
	curCard.setSelected(false);
	curCard= null;
	
	int x = card.getX();
	int y = card.getY();
	int x1 = tempCard.getX();
	int y1 = tempCard.getY();
	if(dir==1){
    
    //横向交换,对应横向移动
		card.setY(y1);
		tempCard.setY(y);
	}else {
    
    //纵向交换,对应纵向移动
		card.setX(x1);
		tempCard.setX(x);
	}
	//交换在2维数组中的对应位置
	cards[x][y]= tempCard;
	cards[x1][y1]= card;
	
	card.move(dir);
	tempCard.move(dir);
}

实现Card类中的move方法

1.记录最新后的位置odx、ody;
2.更odx、ody计算图片运动方向。

public void move(int d) {
    
    
	this.moveFlag=d;
	int dis= 0;
	if(this.moveFlag==1){
    
    //横向交换,对应横向移动
		this.odx=40+y*(32+3)+10;
		dis = this.odx-this.dx;
	}else {
    
    
		this.ody=35+x*(32+3)+10;
		dis = this.ody-this.dy;
	}

	if(dis>0){
    
    //向下运动 、向右运动
		dir = 1;
	}else {
    
    
		dir = -1;
	}
}

修改draw方法

1.根据移动方向和速度更新dx、dy,用来达到移动的目的。
2.当dx>=odx或dx<=odx、dy>=ody或dy<=ody 达到最大移动位置。
3.达到最大移动位置后,要进行消除逻辑处理,写入空方法clear
4.加入边框,表示卡片被选择状态。

//绘制
public void draw(Graphics g) {
    
    
	int index = this.index;

	if(moveFlag!=0){
    
    
		if(this.moveFlag==1){
    
    //横向移动
			dx += dir*speed;//dx修改
			if(dir>0){
    
    
				if(dx>=odx){
    
    //运动到既定位置,停止
					dx = odx;
					moveFlag=0;
					clear();
				}
			}else{
    
    //运动到既定位置,停止
				if(dx<=odx){
    
    
					dx = odx;
					moveFlag=0;
					clear();
				}
			}
		}else {
    
    //纵向移动
			dy += dir*speed;
			if(dir>0){
    
    
				if(dy>=ody){
    
    //运动到既定位置,停止
					dy = ody;
					moveFlag=0;
					clear();
				}
			}else{
    
    
				if(dy<=ody){
    
    //运动到既定位置,停止
					dy = ody;
					moveFlag=0;
					clear();
				}
			}
		}
	}
	//index 默认是0,就是从图片中截取第一个
	int sx1 = index*32;
	int sy1 = 0;
	//截取的右下角计算
	int sx2 = (index+1)*32;
	int sy2 = 32;

	g.drawImage(this.image,dx, dy,dx+width,dy+height,sx1,sy1,sx2,sy2 ,null );	
	if(selected){
    
    
		//绘制边框
		Color oColor = g.getColor(); 
		g.setColor(Color.pink);
		g.drawRect(dx, dy, 32, 32);
		g.setColor(oColor);
	}
}

在这里插入图片描述

消除方法

X方向消除

在这里插入图片描述
以当前为中心,向左向右逐个判断,遇到不同的则返回,遇到相同的则计数器加1,当左边+自己+右边 大于三,则横向需要消除。

//x方向左边计算
private int computedLeftX() {
    
    
	int res = 0;
	Card card;
	//从当前卡片的前一个位置开始往前计算,遇到与当前卡片不是同一类型就直接返回
	for (int i = this.y-1; i >=0; i--) {
    
    
		card = panel.cards[this.x][i];
		if(card==null) continue;
		if(card.pIndex==this.pIndex){
    
    
			res++;
		}else {
    
    
			break;
		}
	}
	return res;
}
//x方向右边计算
private int computedRightX() {
    
    
	int res = 0;
	Card card;
	//从当前卡片的后一个位置开始往后计算,遇到与当前卡片不是同一类型就直接返回
	for (int i = this.y+1; i < panel.COLS; i++) {
    
    
		card = panel.cards[this.x][i];
		if(card==null) continue;
		if(card.pIndex==this.pIndex){
    
    
			res++;
		}else {
    
    
			break;
		}
	}
	
	return res;
}

执行消除

让消除行的上方每一个,依次往下落一格
在这里插入图片描述
上方如果没有了,则新生成一个卡片。

//当前卡片上方全部下落一格
private void down(Card c){
    
    
	Card lastCard;
	//x的循环,表示往上上一个个的取
	for (int i = c.x; i >=0; i--) {
    
    
		if(i==0){
    
    //新创建
			createCard(0,c.y);
			continue;
		}else{
    
    
			lastCard = panel.cards[i-1][c.y];
		}
		if(lastCard==null) {
    
    //新创建
			createCard(i-1,c.y);
		}
		//向下移动
		lastCard.setX(i);
		panel.cards[i][c.y]= lastCard;
		lastCard.move(2);
	}
}

于是X方向消除代码

//x方向消除
private int clearX(int left,int right,int type) {
    
    
	MusicPlayer.disappearMisic();
	//左边消除
	Card card ;
	for (int i = this.y-1; i >= this.y-left ; i--) {
    
    
		card = panel.cards[this.x][i];
		if(card==null) continue;
		card.alive=false;
		panel.cards[this.x][i]=null;
		down(card);
	}
	
	for (int i = this.y+1; i <= this.y+right ; i++) {
    
    
		card = panel.cards[this.x][i];
		if(card==null) continue;
		card.alive=false;
		panel.cards[this.x][i]=null;
		down(card);
	}
	
	if(type==1){
    
    //自己也消除
		this.alive=false;
		panel.cards[this.x][this.y]=null;
		
		down(this);
	}
	
	return 0;
}

在这里插入图片描述

加入Y方向消除代码

private void clearY(int uCount, int dCount) {
    
    
	MusicPlayer.disappearMisic();
	int y = this.y;
	int count = uCount+1+dCount;
	
	int maxX = this.x+dCount;
	int minX = this.x-uCount;
	Card card;
	
	while(count>0){
    
    
		card = panel.cards[maxX][y];
		card.alive=false;
		panel.cards[maxX][y]=null;
		down(card);
		count--;
	}
}

最后

加入积分、游戏结束、重新开始、音效等就完成了

在这里插入图片描述

看到这里的大佬,动动发财的小手 点赞 + 回复 + 收藏,能【 关注 】一波就更好了。

代码获取方式

订阅我的专栏《Java游戏大全》后,可以查看专栏内所有的文章,并且联系博主免费获取其中1-3份你心仪的源代码,专栏的文章都是上过csdn热榜的,值得信赖!专栏内目前有[14]篇实例,未来2个月内专栏会更新到15篇以上,一般2周一更,了解一下我的专栏

猜你喜欢

转载自blog.csdn.net/dkm123456/article/details/118079900