俄罗斯方块后续

在上一篇里,俄罗斯方块实现了简单的向下、左、右移动,在这篇文章中会继续实现旋转消行、设置快速下落显示分数和所消行数暂停重新开始,以及游戏结束的实现。


旋转

在上一篇中,有一个图片标有各个图形各个位置的具体的编号

旋转也是基于0 1 2 3的编号。在旋转的过程中,以标号为0的方块为轴,绕着他进行旋转,记录下每次旋转时,0、1、2、3块的新坐标。在每个图形的类中(即T、S、O类等),增设一个state数组,来记录每个图形存在的四种状态的坐标(其中I、S、Z型有两种状态,O型有一种状态)。

这个state数组就需要一个State类,其中的属性是四个点的坐标,存储四个格子对应的位置。
public class State{/* * 设置8个属性,分别存储四个方块元素的相对位置 */int row0,col0; //轴int row1,col1,row2,col2,row3,col3; //其他点public State(int row0, int col0, int row1, int col1, int row2, int col2, int row3, int col3)
                {super();this.row0 = row0;this.col0 = col0;this.row1 = row1;this.col1 = col1;this.row2 = row2;this.col2 = col2;this.row3 = row3;this.col3 = col3;}
这样通过
states=new State[4];  //图形不同数组大小也不同
即可保存每个图形的所有状态的位置,在之后的编程中,利用state数组存储的坐标改变每个方格的坐标,进而绘制出图形,进行接下来的操作。例如T方块的实现:
public class T extends Tetromino {
	
	//提供构造器进行初始化T型的四格方块的位置//
	public T()
	{
		cells[0]=new Cell(0,4,Tetris.T);
		cells[1]=new Cell(0,3,Tetris.T);
		cells[2]=new Cell(0,5,Tetris.T);
		cells[3]=new Cell(1,4,Tetris.T);
		states=new State[4];
		states[0]=new State(0,0,0,-1,0,1,1,0);
		states[1]=new State(0,0,-1,0,1,0,0,-1);
		states[2]=new State(0,0,0,1,0,-1,-1,0);
		states[3]=new State(0,0,1,0,-1,0,0,1);
	}
}

这一部分存储完成后,我们需要编写两个函数,即rotateLeft()和rotateRight(),rotateRight()是玩家在按下“上”这个键时触发的反应,即将图形向右旋转,而rotateLeft()则是为了在到达底部或者出界时,将已经错误旋转的图形转回来,这种方法与之前文章中左移右移导致出界的解决办法相同。


在获得state状态数组之后,实现rotateRight()变得很简单。
首先我们需要一个旋转计数器count,count每次右转都加1,记录了按了几次“上”键,对应的也就是那四种状态。由count的数值除以4取余,得到了对应的第几个状态的位置,之后利用Cell的get、set函数,即可将对应的坐标改变。rotateLeft()也是同样的方法,但由于左转之前进行了错误的右转,所以需要将右转使count增加的1减掉,即count--,然后再利用Cell类改变对应的坐标。

这些准备工作结束后,我们就在Tetris类中继续加入游戏逻辑。

在获取键盘事件中,增加获取“上”键的case:
case KeyEvent.VK_UP:
	rotateRightAction();
	break;

对应执行 rotateRightAction()函数,在这个函数中,进行旋转的操作,以及出现出界等情况的解决方法。
与之前的判断方式大致相同,同样是判断是否出界或者与其他块重复。但是不同的是,在之前的下落判断出界函数中,没有考虑行方面的出界,即旋转之后导致最上面出界或最下行出界,即需要加入行相关的判断。
private boolean outOfBounds() {
		Cell[] cells=currentOne.cells;
		for(Cell cell:cells) {
			int col=cell.getCol();
			int row=cell.getRow();
			if(col<0||col>9||row<0||row>19)
				return true;
		}
		return false;
	}
public void rotateRightAction() {
		currentOne.rotateRight();
		if(outOfBounds()||coincide())
		{
			currentOne.rotateLeft();
		}
		
	}
至此旋转的相关函数就写完了。

消行


在进行消行操作时,采取的思路是从0-3号逐个遍历他们所在的行是否满行,满行的话则进行消除,将上面的每个格都下移。在这个操作中,存在着较多的问题,所以,先附代码,再进行理解。

public void destroyLine() {
		// 统计销毁行的行数
		int lines = 0;

		Cell[] cells = currentOne.cells;
		for (Cell c : cells) {
			int row = c.getRow();
			while (row < 20) {
				if (isFullLine(row)) {
					lines++;
					wall[row] = new Cell[10];
					for (int i = row; i > 0; i--) {
						System.arraycopy(wall[i - 1], 0, wall[i], 0, 10);
					}
					wall[0] = new Cell[10];
				}
				row++;
			}
		}
		// 从分数池中取出分数,加入总分数
		totalScore += scores_pool[lines];
		totalLine += lines;

	}
public boolean isFullLine(int row) {
		Cell[] line = wall[row];
		for (Cell c : line) {
			if (c == null) {
				return false;
			}
		}
		return true;
	}

函数isFullLine(int row)用来判断该行是否已满。while(row<20)与之后的row++是用来判断每次是否消除了全部可以消除的行,由于在之后的数组复制过程中,只是简单的将上面行的元素赋值给下面的行,而每个位置的方块的位置属性其实并没有改变,所以在这个时候易造成许多问题,利用while循环可以保证该行之下的所有行都不可以消除,避免了在遍历0-3方块的过程中,由于下面的方块可以消除,导致画面下移,但与此同时上面的方块原有的位置本来也可以消除,但是却在移动以后,由于块本身记录的还是所在的原行,而所在的原行已经下移变为下一行,导致这一行没有办法消除。


将上一行的元素向下移动,再重新绘制墙,即可在视觉上实现下移。在下移的实现过程中,还存在一些问题,还需要以后继续改善来进行补充。


快速下落

快速下落的实现即取消了之前设定的程序休眠时间,在触发了快速下落按钮“空格”之后,调用hardDropAction()函数,与start方法实现类似,但是取消了休眠时间,使下一次绘制图形事件发生时,方块的位置已经下落了多格,即在原有设定的0.3秒之内,方块对应所在墙的位置改动较大,则视觉上实现了快速下落。

/*
	 * 一键到底
	 */
	public void hardDropAction() {
		while(true) 
		{
			if(canDrop())
			{
				currentOne.softDrop();
			}
			else
			{
				break;
			}
		}
		LandToWall();
		destroyLine();
		if(!isGameOver())
		{
		currentOne=nextOne;
		nextOne=Tetromino.randomOne();
		}
		else {
			gameState=GAMEOVER;
			
		}
	}

分数、消行数和游戏状态的实现


	int[] score_pool = {0,1,2,5,10};
	private int totalScore=0;
	private int totalLine=0;
对于分数,设置了一个score_pool分数池,即一个数组,代表了消0行得0分,消1行得2分,一次类推,在消行函数成功执行后,设置一个变量,记录每个图形下落后导致多少行被消除,不同得行数即对应不同的分数。
对于消行数,每次消行成功后都将totalLine加1即可。
得到具体数值后,在游戏界面上绘制出来即可。
private void paintLine(Graphics g) {
		g.setFont(new Font(Font.SERIF,Font.ITALIC,30));
		g.drawString("Scores:"+totalScore,335,160);
		
	}
	private void paintScore(Graphics g) {
		g.setFont(new Font(Font.SERIF,Font.ITALIC,30));
		g.drawString("Lines:"+totalLine,335,220);
		
	}
在每个函数的第一行中,定义了绘制的字体和大小,在第二行中,定义了绘制的位置以及绘制的具体字符串,调用了drawString函数。

对于游戏状态。在游戏中由三种状态,即游戏中,暂停和游戏结束。同样在界面上绘制一个gamestate属性来表示现在的进行状态。
String[] showState= {"P[pause]","C[continue]","Return[replay]"};
public static final int PLAYING=0;
public static final int PAUSE=1;
public static final int GAMEOVER=2;
private void paintShowState(Graphics g) {
		g.setFont(new Font(Font.SERIF,Font.ITALIC,30));
		g.drawString(showState[gameState],335,275);
	}
在游戏的切换状态中,showState数组是在界面上显示提示的,在游戏进行中,提示P[pause],在暂停时提示C[continue],在游戏结束后,提示Return[replay]。同样,为了实现游戏状态的切换,需要响应键盘事件。则需要在KeyPressed函数中加入游戏状态切换相关的响应。
if(code==KeyEvent.VK_P) {
	if(gameState==PLAYING)
	gameState=PAUSE;
}
	if(code==KeyEvent.VK_C) {
		if(gameState==PAUSE)
		gameState=PLAYING;			
				}
		if(code==KeyEvent.VK_ENTER){
			gameState=PLAYING;
			wall=new Cell[20][10];
			currentOne=Tetromino.randomOne();
			nextOne=Tetromino.randomOne();
			totalScore=0;
			totalLine=0;}
按下P时,把游戏状态gameState改为PAUSE状态,按下C时,改为PLAYING状态,在按下ENTER时,表示重新开始游戏,将游戏状态改为PLAYING,重新设置空墙,更新现在的块和即将下落的块,以及分数和行数清零。

运行图如下:
附完整代码:
//Tetrimino.java
package com.tetris;
/**
 *  四格方块
 *  属性:
 *    --cells ---四个方块
 *  行为;
 *    moveLeft()
 *    moveRight()
 *    softDrop()
 *
 */
public class Tetromino {
	protected Cell[] cells=new Cell[4];
	//旋转状态属性,状态个数以数组形式存储
	protected State[] states;
	//定义一个变量:充当旋转次数的计数器
	private int count=100000;
 	public void moveLeft()
	{
		for(Cell c:cells)
			c.left();
	}
	public void moveRight()
	{
		for(int i=0;i<4;i++)
			cells[i].right();
	}
	public void softDrop()
	{
		for(int i=0;i<4;i++)
			cells[i].drop();
	}
	/*
	 * 随机生成一个四格方块
	 */
	public static Tetromino randomOne() {
		Tetromino t=null;
		int num=(int)(Math.random()*7);
		switch(num)
		{
		case 0: t=new T();break;
		case 1: t=new O();break;
		case 2: t=new I();break;
		case 3: t=new J();break;
		case 4: t=new L();break;
		case 5: t=new S();break;
		case 6: t=new Z();break;
		}
		return t;
	}
	/*
	 * 顺时针旋转
	 */
	public  void rotateRight()
	{
		//旋转一次计数器加一
		count++;
		State s=states[count%states.length];
		//获取轴的行号和列号
		int row,col;
		Cell c=cells[0];
		row=c.getRow();
		col=c.getCol();
		cells[1].setRow(row+s.row1);
		cells[1].setCol(col+s.col1);
		cells[2].setRow(row+s.row2);
		cells[2].setCol(col+s.col2);
		cells[3].setRow(row+s.row3);
		cells[3].setCol(col+s.col3);
		
	}
	/*
	 * 逆时针旋转
	 */
	public  void rotateLeft()
	{
		count--;
		State s=states[count%states.length];
		//获取轴的行号和列号
		int row,col;
		Cell c=cells[0];
		row=c.getRow();
		col=c.getCol();
		cells[1].setRow(row+s.row1);
		cells[1].setCol(col+s.col1);
		cells[2].setRow(row+s.row2);
		cells[2].setCol(col+s.col2);
		cells[3].setRow(row+s.row3);
		cells[3].setCol(col+s.col3);
	}
	/*
	 * 定义内部类:State,用于封装每次旋转后
	 * 相对于轴的其他三个方块的坐标(行,列)
	 */
	public class State{
		/*
		 * 设置8个属性,分别存储四个方块元素的相对位置
		 */
		int row0,col0; //轴
		int row1,col1,row2,col2,row3,col3; //其他点
		public State(int row0, int col0, int row1, int col1, int row2, int col2, int row3, int col3) {
			super();
			this.row0 = row0;
			this.col0 = col0;
			this.row1 = row1;
			this.col1 = col1;
			this.row2 = row2;
			this.col2 = col2;
			this.row3 = row3;
			this.col3 = col3;
		}
		
	}
}
//Tetris.java
package com.tetris;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.image.BufferedImage;
import java.util.Arrays;

import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;

/*
 * 俄罗斯方块的主类
 * 加载静态资源
 * 前提:必须是面板JPanel,可以嵌入窗口
 * 面板上自带一个画笔,有一个功能:自动绘制
 * 其实是调用了JPanel里的paint()
 */
public class Tetris extends JPanel{
	/*
	 * 属性:正在下落的四格方块
	 *      将要下落的四格方块
	 */
	public Tetromino  currentOne=Tetromino.randomOne();
	private Tetromino  nextOne=Tetromino.randomOne();
	/*
	 * 属性:墙 20*10的方格  宽度为26
	 */
	private Cell[][] wall=new Cell[20][10];
	private static final int CELL_SIZE=26;
	/*
	 * 统计分数
	 */
	int[] score_pool = {0,1,2,5,10};
	private int totalScore=0;
	private int totalLine=0;
	
	/*
	 * 定义三个常量,充当游戏的状态
	 */
	public static final int PLAYING=0;
	public static final int PAUSE=1;
	public static final int GAMEOVER=2;
	/*
	 * 定义一个属性存储游戏的当前状态
	 */
	private int gameState=3;
	
	String[] showState= {"P[pause]","C[continue]","Return[replay]"};
	public static  BufferedImage T;   //T形状的方块 以下以此类推
	public static  BufferedImage I;
	public static  BufferedImage O;
	public static  BufferedImage J;
	public static  BufferedImage L;
	public static  BufferedImage S;
	public static  BufferedImage Z;
	public static  BufferedImage background;  //背景图片
	public static  BufferedImage game_over;
	static {
		try {
			T=ImageIO.read(Tetris.class.getResource("T.png"));
			I=ImageIO.read(Tetris.class.getResource("I.png"));
			O=ImageIO.read(Tetris.class.getResource("O.png"));
			J=ImageIO.read(Tetris.class.getResource("J.png"));
			L=ImageIO.read(Tetris.class.getResource("L.png"));
			S=ImageIO.read(Tetris.class.getResource("S.png"));
			Z=ImageIO.read(Tetris.class.getResource("Z.png"));
			background=ImageIO.read(Tetris.class.getResource("tetris.png"));
			game_over=ImageIO.read(Tetris.class.getResource("game-over.png"));
		}catch(Exception e)
		{
			e.printStackTrace();
		}
	}
	public void paintWall(Graphics g) {
		//外层循环控制行数
		for(int i=0;i<20;i++)
		{
			//内层循环控制列数
			for(int j=0;j<10;j++)
			{
				int x=j*CELL_SIZE;
				int y=i*CELL_SIZE;
				Cell cell=wall[i][j];
				if(cell==null) {
					g.drawRect(x, y, CELL_SIZE, CELL_SIZE);
				}
				else {
					g.drawImage(cell.getImage(),x,y,null);
				}		
			}
		}
		
	}
	public void paintCurrentOne(Graphics g)
		{
			Cell[] cells=currentOne.cells;
				for(int i=0;i<cells.length;i++)
				{
					int x=cells[i].getCol()*CELL_SIZE;
					int y=cells[i].getRow()*CELL_SIZE;
					g.drawImage(cells[i].getImage(), x, y, null);
					}
			}
	//绘制即将下落的另一个
	public void paintNextOne(Graphics g)
	{
		Cell[] cells=nextOne.cells;
		for(Cell cell:cells)
		{
			int col=cell.getCol();
			int row=cell.getRow();
			int x=col*CELL_SIZE+260;
			int y=row*CELL_SIZE+26;
			g.drawImage(cell.getImage(), x, y, null);
			}
	}
	//重写JPanel中的paint方法
	public void paint(Graphics g)
	{
		/*
		 * 背景  g:画笔  g.drawImage(image,x,y,null)
		 * image要绘制的图片
		 * x y 为开始绘制的横纵坐标
		 * 
		 */
		g.drawImage(background, 0, 0,null);
	    //平移坐标轴
		g.translate(15, 15);
		//绘制墙
		paintWall(g);
		//绘制正在下落的四格方块
		paintCurrentOne(g);
		//绘制即将下落的四格方块
		paintNextOne(g);
		paintScore(g);
		paintLine(g);
		paintShowState(g);
		if(gameState==GAMEOVER) {
			g.drawImage(game_over, 0, 0,null);
		}
	}
	private void paintShowState(Graphics g) {
		g.setFont(new Font(Font.SERIF,Font.ITALIC,30));
		g.drawString(showState[gameState],335,275);
		
	}
	private void paintLine(Graphics g) {
		g.setFont(new Font(Font.SERIF,Font.ITALIC,30));
		g.drawString("Scores:"+totalScore,335,160);
		
	}
	private void paintScore(Graphics g) {
		g.setFont(new Font(Font.SERIF,Font.ITALIC,30));
		g.drawString("Lines:"+totalLine,335,220);
		
	}
	/*
	 *封装了游戏的主要逻辑 
	 */
	public void start()
	{
		gameState=PLAYING;
		//开启键盘监听事件
		KeyListener listener=new KeyAdapter() {
			/*
			 * keyPressed()是键盘按钮按下去所调用的方法
			 */
			@Override
			public void keyPressed(KeyEvent e) {
				//获取一个毽子的代号
				int code=e.getKeyCode();
				if(code==KeyEvent.VK_P) {
					if(gameState==PLAYING)
						gameState=PAUSE;
				}
				if(code==KeyEvent.VK_C) {
					if(gameState==PAUSE)
						gameState=PLAYING;
					
				}
				if(code==KeyEvent.VK_ENTER)
				{
					gameState=PLAYING;
					wall=new Cell[20][10];
					currentOne=Tetromino.randomOne();
					nextOne=Tetromino.randomOne();
					totalScore=0;
					totalLine=0;
				}
				switch(code) {
				case KeyEvent.VK_DOWN: 
					softDropAction();
					break;
				case KeyEvent.VK_LEFT:
					moveLeftAction();
					break;
				case KeyEvent.VK_RIGHT:
					moveRightAction();
					break;
				case KeyEvent.VK_UP:
					rotateRightAction();
					break;
				case KeyEvent.VK_SPACE:
					hardDropAction();
					break;
				}
				repaint();
	
			}
		};
		//面板添加事件监听对象listener
		this.addKeyListener(listener);
		//面板对象设置成焦点
		this.requestFocus();
		
		while(true) {
			System.out.println(gameState);
		while(gameState==PLAYING)
		{
			
			/*
			 * 当程序运行到此,会进入休眠状态
			 * 睡眠时间为200毫秒,单位为毫秒
			 * 300毫秒之后,会自动执行后续代码
			 */
			try {
				Thread.sleep(300);
			}catch(InterruptedException e)
			{
				e.printStackTrace();
			}
			if(canDrop())
			{
				currentOne.softDrop();
			}
			else {
				LandToWall();
				destroyLine();
				if(!isGameOver())
				{
				currentOne=nextOne;
				nextOne=Tetromino.randomOne();
				}
				else {
					gameState=GAMEOVER;
					
				}
			}
			/*
			 * 下落之后,需要重新绘制,才会看到下落后的位置
			 * repaint方法发也是JPanel类中 提供的
			 * 此方法调用paint方法
			 */
			repaint();
		}
		}
	}
	/*
	 * 消行,上面的方块都要向下平移
	 */
	public void destroyLine() {
		Cell[] cells=currentOne.cells;
		int count=0;
		for(Cell c:cells) {
		int flag=1;
			int row=c.getRow();
			// Cell[] line=wall[row]	
			//for(Cell r:line)
			for(int i=0;i<10;i++) {
				if(wall[row][i]==null) {
					flag=0;
					break;
				}
			}
			if(flag==1) {
				count++;
				//消行 使用null填满数组元素
				Arrays.fill(wall[row],null);
				for(int j=row;j>0;j--) {
					wall[j]=wall[j-1];
				}
				Arrays.fill(wall[0],null);
			}
			
			/*if(flag==1) {
				wall[row]=new Cell[10];
				for(int i=row;i>0;i--) {
					Cell[] li=wall[i-1];
					
 				}
			}*/
		}
		totalScore+=score_pool[count];
		totalLine+=count;
	}
	public void rotateRightAction() {
		currentOne.rotateRight();
		if(outOfBounds()||coincide())
		{
			currentOne.rotateLeft();
		}
		
	}
	/*
	 * 一键到底
	 */
	public void hardDropAction() {
		while(true) 
		{
			if(canDrop())
			{
				currentOne.softDrop();
			}
			else
			{
				break;
			}
		}
		LandToWall();
		destroyLine();
		if(!isGameOver())
		{
		currentOne=nextOne;
		nextOne=Tetromino.randomOne();
		}
		else {
			gameState=GAMEOVER;
			
		}
	}
	/*
	 * 使用down控制四格方块的下落
	 */
	public void softDropAction() {
		if(canDrop())
		{
			currentOne.softDrop();
		}
		else {
			LandToWall();
			destroyLine();
			if(!isGameOver())
			{
			currentOne=nextOne;
			nextOne=Tetromino.randomOne();
			}
			else {
				gameState=GAMEOVER;
				
			}
		}
	}
	private boolean isGameOver() {
		Cell[] cells=nextOne.cells;
		for(Cell c:cells)
		{
			int row=c.getRow();
			int col=c.getCol();
			if(wall[row][col]!=null) {
				return true;
			}
		}
		return false;
	}
	/*
	 * 使用left键控制向左的行为
	 */
	public void moveLeftAction() {
		//没出界或者没和左面的方块重合
		currentOne.moveLeft();;
		if(outOfBounds()||coincide())
		{
			currentOne.moveRight();;
		}
		}
	private boolean coincide() {
		Cell[] cells=currentOne.cells;
		for(Cell cell:cells) {
			int row=cell.getRow();
			int col=cell.getCol();
			if(wall[row][col]!=null)
			{
				return true;
			}
		}
		return false;
	}
	private boolean outOfBounds() {
		Cell[] cells=currentOne.cells;
		for(Cell cell:cells) {
			int col=cell.getCol();
			int row=cell.getRow();
			if(col<0||col>9||row<0||row>19)
				return true;
		}
		return false;
	}
	public void moveRightAction() {
		//没出界或者右面的方块重合
		currentOne.moveRight();
		//不可以改变这两个函数的位置  因为coincide里的数组不允许参数为-1的时候
		if(outOfBounds()||coincide())
		{
			currentOne.moveLeft();
		}
}
	
	/*
	 * 判断是否下落
	 */
	public boolean canDrop()
	{
		Cell[] cells=currentOne.cells;
		for(Cell cell:cells) {
			/*
			 * 获取每个元素的行号和列号
			 * 判断
			 * 只要有一个元素的下一行有方块
			 * 或者只要有一个元素到达最后一行
			 * 就不能再下落了
			 */
			int row=cell.getRow();
			int col=cell.getCol();
			if(row==19) {
				return false;
			}
			if(wall[row+1][col]!=null) {
				return false;
			}
		}
		return true;
 	}
	/*
	 * 当不能再下落时,需要将四格方块,嵌入到墙中
	 * 也就是存储在二维数组中相应位置中
	 */
	public void LandToWall() {
		Cell[] cells=currentOne.cells;
		for(Cell cell:cells) {
			int row=cell.getRow();
			int col=cell.getCol();
			wall[row][col]=cell;
		}
	} 
	
	//启动游戏的入口
	public static void main(String[] args) {
		//1、创建一个窗口对象
		JFrame frame=new JFrame("俄罗斯方块");
		//创建游戏界面即面板
		Tetris panel=new Tetris();
		//将面板嵌入窗口
		frame.add(panel);
		//2、设置为可见
		frame.setVisible(true);
		//3、设置窗口大小尺寸
		frame.setSize(535, 600);
		//4、设置窗口居中
		frame.setLocationRelativeTo(null);
		//5、设置窗口关闭,即程序终止
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		panel.setBackground(Color.yellow);
		panel.start();	
	}
}




猜你喜欢

转载自blog.csdn.net/weixin_40373090/article/details/80546453