Java俄罗斯方块思想与实现

本文中的代码只是部分代码,全部代码及用到的图片下载地址:链接: https://pan.baidu.com/s/1Ryvr0uxPVX22Ag3aVzGQKQ 密码: tm48

1、我们来分析一下游戏界面,从中能抽象出来:小方块类型,组合方块类型  

先来创建 Cell() 小方块类型,此类代表的时游戏中的最小单位(自身的属性有:row--行号 , col--列号 , image--图片)(行为方法有:left()--左移 , right()--右移 , drop()--下移),下面是具体代码:

 package com.tetris2;

import java.awt.image.BufferedImage;

public class Cell {
	
	/*
	 * 俄罗斯方块中最小单位
	 */
	private int row; //行号
	private int col; //列号
	private BufferedImage image;
	
	public Cell() {
		
	}
	//有参构造器,用来接收小方块的行列坐标和图片
	public Cell(int row, int col, BufferedImage image) {
		super();
		this.row = row;
		this.col = col;
		this.image = image;
	}
	
	public int getRow() {
		return row;
	}
	public void setRow(int row) {
		this.row = row;
	}
	public int getCol() {
		return col;
	}
	public void setCol(int col) {
		this.col = col;
	}
	public BufferedImage getImage() {
		return image;
	}
	public void setImage(BufferedImage image) {
		this.image = image;
	}
	@Override
	public String toString() {
		return "(" + row + ", " + col + ")";
	}
	//小方块向左移动
	public void left(){
		col--;
	}
	//小方块向右移动
	public void rigth(){
		col++;
	}
	//小方块向下移动
	public void drop(){
		row++;
	}
}

2、通过游戏中的7种不同的方块,可以抽象出它们的父类Tetromino(),这7中不同的方块有着共同的特征:都是由4个小方块Cell()组成的,都可以左/右/下移动并且旋转,代码如下:

package com.tetris2;

import java.util.Arrays;

public class Tetromino {
	//组合的方块都是有最小单位的4个方块组成的
	protected Cell[] cells = new Cell[4];
	
	public Cell[] getCells() {
		return cells;
	}
	public void setCells(Cell[] cells) {
		this.cells = cells;
	}
	//方块向左移动
	public void moveLeft(){
		for(Cell c:cells){
			c.left();
		}
	}
	//方块向右移动
	public void moveRigth(){
		for(Cell c:cells){
			c.rigth();
		}
	}
	//方块向下移动
	public void softDrop(){
		for(Cell c:cells){
			c.drop();
		}
	}
	@Override
	public String toString() {
		return "[" + Arrays.toString(cells) + "]";
	}
	
	/*
	 * 随机生成一个四个方块
	 */
	public static Tetromino randomOne(){
		Tetromino t = null;
		int num = (int)(Math.random()*7);//生成0~6随机数,来生成7种不同的随机方块
		switch (num) {
		case 0:t = new T();break;
		case 1:t = new Z();break;
		case 2:t = new O();break;
		case 3:t = new I();break;
		case 4:t = new J();break;
		case 5:t = new L();break;
		case 6:t = new S();break;
		
		}
		return t;
	}
}

3、让7中不同方块子类来继承父类Tetromino()7种方块都有着自己的坐标和图片,所以在写7种方的类时提供一个构造器用来进行初始化(7中不同方块初始位置可参考图Enter.PNG),下面代码只给出T的代码如下:

package com.tetris2;

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

4、还需要定义一个主类Tetris() 来传入方块的图片

    一、显示出一个窗口作为游戏的界面,要绘制出背景,及游戏区域的网格(程序完事后可注释掉)

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, 580);
		// 4:设置窗口居中
		frame.setLocationRelativeTo(null);
		// 5:设置窗口关闭,即程序终止
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		// 游戏的主要逻辑封装在start方法中
		panel.start();

	}

            在创建窗口时要重写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);
		paintState(g);
	}


    二、绘制出正在下落的方块和即将下落的方块(这两个方块时随机生成的),在绘制方块是方块的长款是相同的,这是

           就可以定义一个常量来存放方块的宽,因为每个大方块有4个小方块租场,所以要循环来确定每个小方块的初始下

           落坐标(每个大方块的初始下落的位置是不变的)。代码如下:

public void paintCurrentOne(Graphics g) {
		Cell[] cells = currentOne.cells;
		for (Cell c : cells) {
			int x = c.getCol() * CELL_SIZE;
			int y = c.getRow() * CELL_SIZE;
			g.drawImage(c.getImage(), x, y, null);
		}
	}
public void paintNextOne(Graphics g) {
		// 获取nextOne对象的四个元素
		Cell[] cells = nextOne.cells;
		for (Cell c : cells) {
			// 获取每一个元素的行号和列号
			int row = c.getRow();
			int col = c.getCol();
			// 横坐标和纵坐标
			int x = col * CELL_SIZE + 260;
			int y = row * CELL_SIZE + 26;
			g.drawImage(c.getImage(), x, y, null);
		}
	}


    三、让大方块开始下落,停止下落条件:大方块到达底部  或  大方块中任意一个小方块下面有小方块时,大方块停止下落,当大方块停止下落时,将大方块嵌入到墙中,在下落时,要进行进程休眠,这样才能看到下落过程。

          停止下落停止下落有两种状态:

                        1是方块到底后停止(判断大方块中的小方块有任意一个坐标到达底边界时,停止下落方块)

                        2是碰到其他方块(这个判断是要在下方已有下落到底的方块的前提下进行的,方块下落倒底后,

                                                    会嵌入到墙的二维数组中,之后在下落的方块只需判断在下落中的大方块下方的

                                                    墙的二维数组是否为空就行,不为空就无法在下落了)

                    代码如下:

while (true) {
			/**
			 * 当程序运行到此,会进入睡眠状态, 睡眠时间为300毫秒,单位为毫秒 300毫秒后,会自动执行后续代码
			 */
			try {
				Thread.sleep(800);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

			if (game_state == PLAYING) {
				if (canDrop()) {
					currentOne.softDrop();
				} else {
					landToWall();
					destroyLine();
					// 将下一个下落的四格方块赋值给正在下落的变量
					if (!isGameOver()) {
						currentOne = nextOne;
						nextOne = Tetromino.randomOne();
					} else {
						game_state = GAMEOVER;
					}
				}
				repaint();
				/*
				 * 下落之后,要重新进行绘制,才会看到下落后的 位置 repaint方法 也是JPanel类中提供的 此方法中调用了paint方法
				 */
			}
		}

    四、开启键盘监听事件来控制方块的移动(下左右翻转都要判断停止下落)

        (1)向下:判断能否继续下落,调用下落方法,每次按键调用后都要重新绘制,以保证画面流畅

public void softDropAction() {
		if (canDrop()) {
			currentOne.softDrop();
		} else {
			landToWall();
			destroyLine();
			currentOne = nextOne;
			nextOne = Tetromino.randomOne();
		}
	}

        (2)向左:如果大方块不越界,或不与其它方块重合,就可向左移动(如果出现数组下标越界异常,可以先向左移,

                如果越界,再向右移回来)

protected void moveLeftAction() {
		currentOne.moveLeft();
		if (outOfBounds() || coincide()) {
			currentOne.moveRight();
		}
	}

       (3)向右:和向左原理相同但方向不同

       (4)旋转:方块旋转的方向是顺时针的,不同方块有着不同的旋转后的状态(T,L,J行方块有4种,I,S,

                        Z行方块有2种,O型方块有一种)因为大方块是由4格小方块组成,所以大方块旋转就是小方块

                        的位置发生变化,我们要定义一个类来存放大方块在旋转时,小方块的相对坐标位置,并且要在

                        每一个大方块类中写出不同状态下1-3号方块相对于0号方块的相对位置(每一个大方块都有4个小

                        方块,每个小方块都有行列坐标,所以要定义8个属性来存放小方格的坐标),想要确定当前大方

                        块的旋转位置,需要统计旋转按键的点击次数, 之后和4进行取余(因为大方块最多只有4种旋转状态)

                        旋转方块时是以其中的一个小方块为轴来进行旋转的,所以在旋转之前要确定轴的行号和列号

                        (1-3号小方块的坐标为行/列 + 相对坐标)

                    注:方块的相对坐标参考图“Enter.PNG”。参考代码:

public void rotateRight() {
		//旋转有一次,计算器增长1
		count++;//100001
		State s = states[count%states.length];
		//需要获取轴的行号和列号
		Cell c = cells[0];
		int row = c.getRow();
		int 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 class State{
		/**
		 * 设计八个属性,分别存储四格方块元素的相对
		 * 位置
		 */
		int row0,col0,row1,col1,row2,col2,row3,col3;
		public State() {}
		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;
		}
}

       (5)快速下落多次无休眠调用方块下移(要判断方块能否下落),当大方块不能下移时,将大方块嵌入到墙中.

            代码如下:

public void softDropAction() {
		if (canDrop()) {
			currentOne.softDrop();
		} else {
			landToWall();
			destroyLine();
			currentOne = nextOne;
			nextOne = Tetromino.randomOne();
		}
	}

       (6)满行消除下落

           方法1:

                           清空行:循环检查下落后的四格方块大行坐标,根据大方块下落停止后所在的位置的行号,

                                        还循环判断这个行号上对应的墙上的数组是否为空,如果为空,则表示次行未满,

                                        跳出循环判断下一行。

                           向下平移行:将清空后的行的上一行的空满状态赋值给清空的这一行,以此类推直到最上面一行,

                                         在清空行和向下平移行外面加一个row<20的循环,这样可以避免在清空行并且向下

                                        平移后行号改变,导致的原来行上已满但平移后原来已满的行的行号现在没有满,

                                        导致的不能被清空的现象。

            方法2:
            先进行判断行是否满了,满了后进行清空,并记住行号,之后再从小的行号开始向下平移。
            方法3:

            先找出大方块停止下落后中的小方块所在最小的行,以最小的行开始到最后一行结束为循环,

                            进行清空行和平移行操作;

            方法1代码:

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;

	}
       (7)游戏状态:
                游戏状态有 结束(GAMEOVER = 2),暂停(PAUSE = 1),从新开始(PLAYING = 0)
                结束:

                      如果下一个大方块初始的位置的墙上不为空,游戏结束,显示结束画面;

                             (结束的判断要加在,自动下落,快速下落处)

                暂停:
                       在自动下落中添加一条判断游戏状态的if语句=1时游戏暂停(也就是不走下落的方法);
                重新开始:
                        游戏结束后显示结束界面,点击重新开始后,墙清空,下落开始


       (8)游戏计分/消行数:

                消除不同数量的行分数是不相同的;分数与消行数在从新开始后清0;



猜你喜欢

转载自blog.csdn.net/woainiqazwsx123/article/details/80464205