Java游戏开发——连连看

游戏介绍:

      “连连看”是一款来源于我国台湾的桌面小游戏,主要考验的是玩家们的眼力,在有限的时间内,只要能把所有能连接的相同图案,两个两个的找出来,每找到一对,它们就会自动消失,只要能把所有的图案全部消完即可获得胜利。所谓能够连接,是指无论横向还是纵向,从一个图案到另一个图案之间的连线拐角不能超过两个(中间的直线不超过三根),其中连线不能从尚未消去的图案上经过。

本次开发的连连看游戏运行效果如下图所示,游戏具有统计消去方块个数、打乱现有方块位置、智能辅助以及重开一局的功能。

使用到的素材文件夹如下:

 

素材及源码链接:https://pan.baidu.com/s/1GEzRACA2PMjFYJZS7hMvTA 提取码: yc21

游戏数据模型:

连连看的游戏界面是一个N*M的网格地图,每个网格显示一张图片;网格地图的信息使用二维数组来存储,每个数组元素存储对应网格地图中的每一个格子里的图片ID,如果图片ID非-1(BLANK_STATE)则绘制图片。

动物方块布局:

游戏地图信息初始化时,由于方块必须成对出现,需要引入一个临时的动态数组list,该list用来存储地图所有的图案ID信息,在这里我们是制作10*10的网格地图,一共10种图案,所以可以先向list里添加10组完全一样的图案ID,每组10个;创建二维数组map存储网格地图信息,初始化map里的每个数组元素为-1(BLANK_STATE),然后遍历map,按遍历顺序依次随机从list中取一个图案ID元素放入map并从list中移出刚才取出来的元素,遍历完成后返回map;代码实现如下:

	public int[][] getMap(){
		
		ArrayList<Integer> list = new ArrayList<Integer>();//先将等量图片ID添加到list中
		
		for(int i=0;i<n*n/10;i++){			
			for(int j=0;j<count;j++){//每个图案种类的ID各添加一个,循环10次
			list.add(j);
			}			
		}		
		
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			int index = (int) (Math.random()*list.size());//从list中随机取一个图片ID,并将其添加到数组中,再从list中删除掉它
			map[i][j] = list.get(index);
			list.remove(index);	
		}
	}

	return map;//返回一个图片随机生成的地图数组

	}

连通算法:

①直连方式

在直连方式中,要求两个选中的方块在同一行或者同一列(图1-1,图1-2),并且之间没有其他任何图案的方块,实现最简单。

②单拐点连通

相当于通过两个选中的方块划出一个矩形,两个方块是一对矩形的对角顶点,另外两个顶点中的某个顶点如果为BLANK_STATE并且可以同时与这两个方块直连,那就说明可以单拐点连通(图1-3)。

③双拐点连通

这种方式的两个拐点z1,z2必定在两个选中的方块p1,p2所在的水平方向或者垂直方向的直线上(图1-4,图1-5)。

按p1(x1,y1)点向4个方向探测(此处的x1,y1为数组下标),例如向右探测,每次y1+1,判断(x1,y1+1)与p2(x2,y2)点可否形成单拐点连通性,如果可以形成连通,则两个拐点连通;如果超出图形右边界区域,则还需要判断两个拐点在选中方块的右侧,且两个拐点在图案区域之外连通的情况是否存在。这里可以简化为判断p2点(x2,y2)是否可以水平直通到右边界(图1-6)

图1-1图1-2图1-3图1-4图1-5图1-6

 

经过上面的分析,对两个选中的方块是否可以消去算法流程图如下:

该功能属于鼠标点击事件的一部分,代码如下:

	@Override
	public void mousePressed(MouseEvent e) {
		// TODO Auto-generated method stub
		
		Graphics g = this.getGraphics();
		
		int x = e.getX()-leftX;//点击位置x-偏移量x
		int y = e.getY()-leftY;//点击位置y-偏移量y
		int i = y/50;//对应数组行数,根据像素坐标转换成数组下标坐标
		int j = x/50;//对应数组列数
		if(x<0||y<0)//超出地图范围
			return ;
		
	
		
		if(isClick){//第二次点击
		
			if(map[i][j]!=BLANK_STATE){
				if(map[i][j]==clickId){//点击的是相同图片Id,但不是重复点击同一图片
					if(i==clickX&&j==clickY)
					return ;
					
					if(verticalLink(clickX,clickY,i,j)||horizontalLink(clickX,clickY,i,j)||oneCornerLink(clickX,clickY,i,j)||twoCornerLink(clickX,clickY,i,j)){//如果可以连通,画线连接,然后消去选中图片并重置第一次选中标识						
						drawSelectedBlock(j*50+leftX,i*50+leftY,g);
						drawLink(clickX,clickY,i,j);//画线连接
						isClick = false;	

					}else{
						clickId = map[i][j];//重新选中图片并画框
						clearSelectBlock(clickX,clickY,g);
						clickX = i;
						clickY = j;
						drawSelectedBlock(j*50+leftX,i*50+leftY,g);
					}
					
				}else{
					clickId = map[i][j];//重新选中图片并画框
					clearSelectBlock(clickX,clickY,g);
					clickX = i;
					clickY = j;
					drawSelectedBlock(j*50+leftX,i*50+leftY,g);
				}
				
			}
			
		}else{//第一次点击
			if(map[i][j]!=BLANK_STATE){
				//选中图片并画框
				clickId = map[i][j];
				isClick = true;
				clickX = i;
				clickY = j;
				drawSelectedBlock(j*50+leftX,i*50+leftY,g);
			}
		}
	}

其中isClick是用来标识是否第一次选中图案,clickId表示第一次选择图案对应的ID,clickX表示第一次选中图案的行下标,clickY表示第一次选中图案的列下标,如果第二次选中的图案与第一次选中的图案不同,重新选中;如果两次选择的图案相同,但是不连通,重新选中第二次选中的图片。

直连方式分为水平连通和垂直连通两种方式,分别使用horizontalLink()和verticalLink()进行判断:

	//判断是否可以水平相连
	private boolean horizontalLink(int clickX1, int clickY1, int clickX2, int clickY2) {
		
		if(clickY1>clickY2){//保证y1<y2
			int temp1 = clickX1;
			int temp2 = clickY1;
			clickX1 = clickX2;
			clickY1 = clickY2;
			clickX2 = temp1;
			clickY2 = temp2;
		}
		
		if(clickX1==clickX2){//如果两个选中图片的所在行数相同,说明可能可以水平相联
			
			for(int i=clickY1+1;i<clickY2;i++){
				if(map[clickX1][i]!=BLANK_STATE){//如果两图片中间还有其他图片,说明不能直接水平相连
					return false;
				}
			}
			
			linkMethod = LINKBYHORIZONTAL;
			return true;
		}
		
		
		return false;
	}


	//判断是否可以垂直连接
	private boolean verticalLink(int clickX1, int clickY1, int clickX2, int clickY2) {
		
		if(clickX1>clickX2){//保证x1<x2
			int temp1 = clickX1;
			int temp2 = clickY1;
			clickX1 = clickX2;
			clickY1 = clickY2;
			clickX2 = temp1;
			clickY2 = temp2;
		}
		
		if(clickY1==clickY2){//如果两个选中图片的所在列数相同,说明可能可以垂直相联
			
			for(int i=clickX1+1;i<clickX2;i++){
				if(map[i][clickY1]!=BLANK_STATE){//如果两图片中间还有其他图片,说明不能直接垂直相连
					return false;
				}
			}

			linkMethod = LINKBYVERTICAL;
			

			return true;
		}
		
		
		
		return false;
	}

单拐点连通使用oneCornerLink()方法实现判断(z1存储拐点数组下标信息):

//判断是否可以通过一个拐点相连
	private boolean oneCornerLink(int clickX1, int clickY1, int clickX2, int clickY2) {
		
		if(clickY1>clickY2){//保证(x1,y1)是矩形的左上角或者左下角
			int temp1 = clickX1;
			int temp2 = clickY1;
			clickX1 = clickX2;
			clickY1 = clickY2;
			clickX2 = temp1;
			clickY2 = temp2;
		}
		
		if(clickX1<clickX2){//如果(x1,y1)位于矩形左上角
			
			//判断右上角是否为空并且可以直接与(x1,y1)和(x2,y2)相连接,(clickX1, clickY2)是右上角拐点下标
			if(map[clickX1][clickY2]==BLANK_STATE&&horizontalLink(clickX1, clickY1, clickX1, clickY2)&&verticalLink(clickX2,clickY2,clickX1,clickY2)){
				linkMethod = LINKBYONECORNER;
				z1 = new Node(clickX1,clickY2);
				return true;
			}
			
			//判断左下角是否为空并且可以直接与(x1,y1)和(x2,y2)相连接,(clickX2, clickY1)是左下角拐点下标
			if(map[clickX2][clickY1]==BLANK_STATE&&horizontalLink(clickX2, clickY2, clickX2, clickY1)&&verticalLink(clickX1,clickY1,clickX2, clickY1)){
				linkMethod = LINKBYONECORNER;
				z1 = new Node(clickX2,clickY1);
				return true;
			}
			
		}else{//如果(x1,y1)位于矩形左下角
			
			//判断左上角是否为空并且可以直接与(x1,y1)和(x2,y2)相连接,(clickX2, clickY1)是左上角拐点下标			
			if(map[clickX2][clickY1]==BLANK_STATE&&horizontalLink(clickX2, clickY2, clickX2, clickY1)&&verticalLink(clickX1,clickY1,clickX2, clickY1)){
				linkMethod = LINKBYONECORNER;
				z1 = new Node(clickX2,clickY1);
				return true;				
			}
			
			//判断右下角是否为空并且可以直接与(x1,y1)和(x2,y2)相连接,(clickX1, clickY2)是右下角拐点下标			
			if(map[clickX1][clickY2]==BLANK_STATE&&horizontalLink(clickX1, clickY1, clickX1, clickY2)&&verticalLink(clickX2,clickY2,clickX1, clickY2)){
				linkMethod = LINKBYONECORNER;
				z1 = new Node(clickX1,clickY2);
				return true;				
			}
				
		}
			
		return false;
	}

双拐点连通使用twoCornerLink()方法实现判断。按p1(clickX1,clickY1)点向4个方向探测新的z1点与p2(clickX2,clickY2)能否行程单拐点连通性(z1,z2存储两个拐点的数组 下标信息):

	//判断是否可以通过两个拐点相连
	private boolean twoCornerLink(int clickX1, int clickY1, int clickX2, int clickY2) {
		
		//向上查找
		for(int i=clickX1-1;i>=-1;i--){
			
			//两个拐点在选中图案的上侧,并且两个拐点在地图区域之外
			if(i==-1&&throughVerticalLink(clickX2, clickY2, true)){
				z1 = new Node(-1,clickY1);
				z2 = new Node(-1,clickY2);
				linkMethod = LINKBYTWOCORNER;
				return true;
			}
			
			if(i>=0&&map[i][clickY1]==BLANK_STATE){
				
				if(oneCornerLink(i, clickY1, clickX2, clickY2)){
					linkMethod = LINKBYTWOCORNER;
					z1 = new Node(i,clickY1);
					z2 = new Node(i,clickY2);
					return true;
				}
				
			
			}else{
				break;
			}
			
		}
		
		//向下查找
		for(int i=clickX1+1;i<=n;i++){
			
			//两个拐点在选中图案的下侧,并且两个拐点在地图区域之外
			if(i==n&&throughVerticalLink(clickX2, clickY2, false)){
				z1 = new Node(n,clickY1);
				z2 = new Node(n,clickY2);
				linkMethod = LINKBYTWOCORNER;
				return true;
			}
			
			if(i!=n&&map[i][clickY1]==BLANK_STATE){
				
				if(oneCornerLink(i, clickY1, clickX2, clickY2)){
					linkMethod = LINKBYTWOCORNER;
					z1 = new Node(i,clickY1);
					z2 = new Node(i,clickY2);
					return true;
				}
			
			}else{
				break;
			}
			
		}
		
		
		//向左查找
		for(int i=clickY1-1;i>=-1;i--){

			//两个拐点在选中图案的左侧,并且两个拐点在地图区域之外
			if(i==-1&&throughHorizontalLink(clickX2, clickY2, true)){
				linkMethod = LINKBYTWOCORNER;
				z1 = new Node(clickX1,-1);
				z2 = new Node(clickX2,-1);
				return true;
			} 
			
			
			if(i!=-1&&map[clickX1][i]==BLANK_STATE){
				
				if(oneCornerLink(clickX1, i, clickX2, clickY2)){
					linkMethod = LINKBYTWOCORNER;
					z1 = new Node(clickX1,i);
					z2 = new Node(clickX2,i);
					return true;
				}
			
			}else{
				break;
			}
			
		}
		
		//向右查找
		for(int i=clickY1+1;i<=n;i++){

			//两个拐点在选中图案的右侧,并且两个拐点在地图区域之外
			if(i==n&&throughHorizontalLink(clickX2, clickY2, false)){
				z1 = new Node(clickX1,n);
				z2 = new Node(clickX2,n);
				linkMethod = LINKBYTWOCORNER;
				return true;
			}
			
			if(i!=n&&map[clickX1][i]==BLANK_STATE){
				
				if(oneCornerLink(clickX1, i, clickX2, clickY2)){
					linkMethod = LINKBYTWOCORNER;
					z1 = new Node(clickX1,i);
					z2 = new Node(clickX2,i);
					return true;
				}
				
			}else{
				break;
			}			
			
		}
		
		
		return false;
	}

throughHorizontalLink()用于水平方向判断边界的连通性,如flag为true,则从(x,y)点水平向左直到边界,判断是否全部为空块BLANK_STATE;如果flag为false,则从(x,y)点水平向右直到边界,判断是否全部为空块BLANK_STATE。

//根据flag,判断(x1,y1)左右两侧中的一侧是否还有其他图片,如果没有,可以相连
	private boolean throughHorizontalLink(int clickX, int clickY,boolean flag){

		if(flag){//向左查找
			
			for(int i=clickY-1;i>=0;i--){
				if(map[clickX][i]!=BLANK_STATE){
					return false;
				}
			}			
			
		}else{//向右查找
			
			for(int i=clickY+1;i<n;i++){
				if(map[clickX][i]!=BLANK_STATE){
					return false;
				}
			}
			
		}
		
		return true;
	}

throughVerticalLink()用于水平方向判断边界的连通性,如flag为true,则从(x,y)点水平向上直到边界,判断是否全部为空块BLANK_STATE;如果flag为false,则从(x,y)点水平向下直到边界,判断是否全部为空块BLANK_STATE。


	//根据flag,判断(x1,y1)上下两侧中的一侧是否还有其他图片,如果没有,可以相连
	private boolean throughVerticalLink(int clickX,int clickY,boolean flag){
		
		if(flag){//向上查找
			
			for(int i=clickX-1;i>=0;i--){
				if(map[i][clickY]!=BLANK_STATE){
					return false;
				}
			}
			
		}else{//向下查找
			
			for(int i=clickX+1;i<n;i++){
				if(map[i][clickY]!=BLANK_STATE){
					return false;
				}
			}
			
		}
	
		
		return true;
	}

智能查找功能实现(按D键触发):

首先先判断在此之前玩家有没有选定图案,如果有清空选定。

选择第一个方块:

①从第i行第j列从左向右、从上到下式查找,如果map[i][j]不为空,选定第一个图案并记录选中ID和数组下标。

选择第二个方块:

②从第p行第q列也是从左向右、从上到下式查找(初始p=i,q=j),如果map[i][j]==map[p][q]并且两次选中的图案对应数组下标不是完全相等,判断(p,q)和(i,j)是否可以连通,如果可以连通,对两个方块进行画框并连线。如果两层循环下来没找到可以连通的方块,重新选定第一个方块。

如果四层循环均未找到连通的方块,返回false。

//提示,如果有可以连接的方块就消去并且返回true
	private boolean find2Block() {
		
		
		if(isClick){//如果之前玩家选中了一个方块,清空该选中框
			clearSelectBlock(clickX, clickY, this.getGraphics());
		isClick = false;
		}
		
		for(int i=0;i<n;i++){
			for(int j=0;j<n;j++){
			
				
				if(map[i][j]==BLANK_STATE){
					continue;
				}				
				
				for(int p=i;p<n;p++){
					for(int q=0;q<n;q++){
						  if(map[p][q]!=map[i][j]||(p==i&&q==j)){//如果图案不相等或者重复选择同一个方块
							  continue;
						  }						  
						  
						  if(verticalLink(p,q,i,j)||horizontalLink(p,q,i,j)
								  ||oneCornerLink(p,q,i,j)||twoCornerLink(p,q,i,j)){
							  drawSelectedBlock(j*50+leftX, i*50+leftY, this.getGraphics());
							  drawSelectedBlock(q*50+leftX, p*50+leftY, this.getGraphics());
							  drawLink(p, q, i, j);
							  repaint();
							  return true;
						  }
				
					}
				}				
			}
		}
		
		isWin();
		
		return false;
	}

动物图案的显示:

存储网格地图信息的二维数组map里存储的其实是图片ID,还需要将其转换成对应的图片。在这里使用Image数组pic存放图片,图片资源的获取及显示如下:

//初始化图片数组
private void getPics() {
		pics = new Image[10];
		for(int i=0;i<=9;i++){
			pics[i] = Toolkit.getDefaultToolkit().getImage("D:/Game/LinkGame/pic"+(i+1)+".png");
		}		
	}


public void paint(Graphics g){
		g.clearRect(0, 0, 800, 30);
		
		for(int i=0;i<n;i++){
			for(int j=0;j<n;j++){
				if(map[i][j]!=BLANK_STATE){
					g.drawImage(pics[map[i][j]],leftX+j*50,leftY+i*50,50,50,this);
				}else{
					g.clearRect(leftX+j*50,leftY+i*50,50,50);
				}
			}
		}
		
	}

在这里如果图片不是空块,则按照图片ID直接绘制图案,每个方块的宽度和高度是50,leftX是左上角网格地图起始X像素坐标,

leftY是左上角起始Y像素坐标。

 

给选定图案画选中框以及清空选中框:

方块的大小是50*50,在这里对转换后的方块左上角起点(x,y)像素坐标画框,为什么在(x+1,y+1)处画宽度高度为48像素的矩形呢?因为这次使用了局部刷新方法,清除选中框时,clearSelectedBlock()会重画(x,y)处的图案,这样48*48的选中框就可以在重画过程中顺利的被清除了。据说这样做可以消除闪烁。。。

	//画选中框,此处x,y为转换后的像素坐标
	private void drawSelectedBlock(int x, int y, Graphics g) {
		Graphics2D g2 = (Graphics2D) g;//生成Graphics对象
		BasicStroke s = new BasicStroke(1);//宽度为1的画笔
		g2.setStroke(s);
		g2.setColor(Color.RED);
		g.drawRect(x+1, y+1, 48, 48);
	}

	public void clearSelectBlock(int i,int j,Graphics g){
		g.clearRect(j*50+leftX, i*50+leftY, 50, 50);
		g.drawImage(pics[map[i][j]],leftX+j*50,leftY+i*50,50,50,this);
//		System.out.println("清空选定"+i+","+j);
	}

画线及延时功能:

首先先将传过来的数组下标进行中心点的转换,例如map[3][2]对应的方块左上角坐标应该是(2*50+leftX,3*50+leftY),它的中心点坐标应该在此基础上增加半个方块长宽,即(2*50+leftX+25,3*50+leftY+25);经过这样的转换,就可以得到两个选中的方块对应的方块中心坐标p1,p2了。

根据连通方式,进行线条的绘画。

①水平连通或者垂直连通:直接连接p1,p2;

②单拐点连通:将z1和p1,z1和p2进行连接;

③双拐点连通:如果p1与拐点z1不在同一行或者同一列,先将z1,z2进行交换。再将z1和p1,z2和p2,z1和z2连接。

延时功能:

使用Thread.currentThread().sleep(500);做到画线后延时500ms再消去方块。

 

	//画线,此处的x1,y1,x2,y2二维数组下标
	@SuppressWarnings("static-access")
	private void drawLink(int x1, int y1, int x2, int y2) {

		Graphics g = this.getGraphics();
		Point p1 = new Point(y1*50+leftX+25,x1*50+leftY+25);
		Point p2 = new Point(y2*50+leftX+25,x2*50+leftY+25);
		if(linkMethod == LINKBYHORIZONTAL || linkMethod == LINKBYVERTICAL){
			g.drawLine(p1.x, p1.y,p2.x, p2.y);
			System.out.println("无拐点画线");
		}else if(linkMethod ==LINKBYONECORNER){
			Point point_z1 = new Point(z1.y*50+leftX+25,z1.x*50+leftY+25);//将拐点转换成像素坐标
			g.drawLine(p1.x, p1.y,point_z1.x, point_z1.y);
			g.drawLine(p2.x, p2.y,point_z1.x, point_z1.y);
			System.out.println("单拐点画线");			
		}else{
			Point point_z1 = new Point(z1.y*50+leftX+25,z1.x*50+leftY+25);
			Point point_z2 = new Point(z2.y*50+leftX+25,z2.x*50+leftY+25);
			
			if(p1.x!=point_z1.x&&p1.y!=point_z1.y){//保证(x1,y1)与拐点z1在同一列或者同一行
				Point temp;
				temp = point_z1;
				point_z1 = point_z2;
				point_z2 = temp;
			}

			g.drawLine(p1.x, p1.y, point_z1.x, point_z1.y);
			g.drawLine(p2.x, p2.y, point_z2.x, point_z2.y);
			g.drawLine(point_z1.x,point_z1.y, point_z2.x, point_z2.y);
			
			System.out.println("双拐点画线");			
		}
		
		count+=2;//消去的方块数目+2
		GameClient.textField.setText(count+"");
		try {
			Thread.currentThread().sleep(500);//延时500ms
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		repaint();
		map[x1][y1] = BLANK_STATE;
		map[x2][y2] = BLANK_STATE;
		isWin();//判断游戏是否结束
	}

输赢判断:

使用isWin()实现,如果当前消去的方块是10*10个,则游戏结束。

private void isWin() {
		
		if(count==n*n){
			String msg = "恭喜您通关成功,是否开始新局?";
			int type = JOptionPane.YES_NO_OPTION;
			String title = "过关";
			int choice = 0;
			choice = JOptionPane.showConfirmDialog(null, msg,title,type);
			if(choice==1){
				System.exit(0);
			}else if(choice == 0){
				startNewGame();
			}
		}
		
	}

打乱现有方块顺序(按A键触发):

这个功能跟前面的随机生成网格地图信息的实现过程很类似,就不多解释了。

public int[][] getResetMap(){//获取再次打乱后的地图信息
		
		ArrayList<Integer> list = new ArrayList<Integer>();//list用来存储原先的地图信息
		
		for(int i=0;i<n;i++){
			for(int j=0;j<n;j++){
				if(map[i][j]!=-1)//如果(x,y)处的图片ID不为-1,那么将该图片id添加到list
					list.add(map[i][j]);		
				map[i][j]=-1;
			}
		}
		
		//将原先地图上剩余的未消去的图片打乱
		while(!list.isEmpty()){
			
			int	index = (int) (Math.random()*list.size());//从list中随机取一个图片ID,并将其添加到数组中,再从list中删除掉它
			boolean flag = false;
			
			while(!flag){
				int i = (int) (Math.random()*n);//获取随机的地图行列
				int j = (int) (Math.random()*n);
				if(map[i][j]==-1){//如果该位置无图片
					map[i][j] = list.get(index);
					list.remove(index);
					flag = true;
				}	
			}
			
		}
		
		return map;
		
	}

重开一局:

初始化游戏面板数据,new地图对象,重新绘画即可:

public void startNewGame() {
		// TODO Auto-generated method stub
		count = 0;
		mapUtil = new Map(10,n);		
		map = mapUtil.getMap();
		isClick = false;
		clickId = -1;
		clickX = -1;
		clickY = -1;
		linkMethod = -1;
		GameClient.textField.setText(count+"");
		repaint();
	}

到这里,连连看游戏开发的核心功能实现已经全部介绍完毕了。

由于本次开发的连连看游戏源代码篇幅过长,所以在这里我就不再贴完整源代码了,有需要的可以在素材链接里下载。

如果大家有什么建议或者对这篇博客还有疑问的话可以在评论处一起讨论,感谢支持~

素材及源码链接:https://pan.baidu.com/s/1GEzRACA2PMjFYJZS7hMvTA 提取码: yc21

阅读更多

没有更多推荐了,返回首页