Java:俄罗斯方块JPanel版

首先7种俄罗斯方块都是由4块小方块组成的,所以我们可以抽象出一个Cell类:

  • 考虑到的整个游戏是在一个坐标轴中完成的,初步设定为10X20原点于左上角,向右为x轴向下为y轴的坐标轴,所以每一个Cell都应该有坐标值;
  • 不同的方块颜色不同,所以Cell应该还要有BufferedImage属性用来显示我们已经准备好的颜色图片。
  • 俄罗斯方块都有向左、向右、向下的行为,不过是每一个小方块共同行为,所以Cell类中也应该有向左向右向下的行为;

Cell类:

import java.awt.image.BufferedImage;

public class Cell {
	private int x;
	private int y;
	private BufferedImage image;
	
	public Cell() {
		super();
	}
	
	public Cell(int x, int y, BufferedImage image) {
		super();
		this.x = x;
		this.y = y;
		this.image = image;
	}

	public int getX() {
		return x;
	}

	public void setX(int x) {
		this.x = x;
	}

	public int getY() {
		return y;
	}

	public void setY(int y) {
		this.y = y;
	}

	public BufferedImage getImage() {
		return image;
	}

	public void setImage(BufferedImage image) {
		this.image = image;
	}
	
	public void cellMoveLeft(){
		this.y--;
	}
	
	public void cellMoveRight(){
		this.y++;
	}
	
	public void cellMoveDown(){
		this.x++;
	}
	
}

4个Cell即Cell数组作为俄罗斯方块Tetris类的一个属性

  • 考虑到每种俄罗斯方块的初始位置已经形状不同,我们应该在7种具体类中实例化4个方块的具体位置,所以Cell数组修饰词应该是protected;
  • 俄罗斯方块即方块组的行为是通过每一个小方块的共同行为实现的;
  • 应该有一个随机生成7种俄罗斯方块之一的方块,并且在这个方法中实例化随机到的俄罗斯方块类型;

Tetris类中以上部分

public class Tetris {

	protected Cell[] cells;
	
	public Tetris(){
		this.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.cellMoveLeft();
		}
	}

	public void moveRight(){
		for(Cell c:cells){
			c.cellMoveRight();
		}
	}
	
	public void moveDown(){
		for(Cell c:cells){
			c.cellMoveDown();
		}
	}
	
	public Tetris randomTetris(){
		Tetris t=null;
		switch ((int)(Math.random()*7)) {
		case 0:
			t=new I();
			break;
		case 1:
			t=new O();
			break;
		case 2:
			t=new T();
			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;
	}
	
}

然后是最关键的旋转

俄罗斯方块旋转的过程中,总会有1个方块其位置是不变的,我们不妨将这个不变的方块当做轴心,所以我们在具体类实例化的时候应该将Cell数组的第一个元素当做轴心,其他方块旋转后的位置都是相对于轴心,所以我们可以把每次旋转后的相对位置存储在一个State数组中。也考虑到不同类型的俄罗斯方块旋转形态种类不同(比如I只有两种形态,O只有一种形态即不能旋转),而且应该在具体类中实例化,所以State[]数组的修饰词也应该是protected。

我们可以设置一个较大的计数器,向左旋转即计数器-1,向右+1,通过计数器对数组长度取余来指定State数组中的相对状态。如果通过随机State数组中的元素就不能达到向左向右旋转后回到原始状态的效果,所以才需要一个State数组来"记忆"旋转顺序。

Tetris类中旋转部分

protected State[] states;
int rotateCount=1000;

    public class State{
		int x0,y0,x1,y1,x2,y2,x3,y3;
		public State(int x0, int y0, int x1, int y1, int x2, int y2, int x3, int y3) {
			super();
			this.x0 = x0;
			this.y0 = y0;
			this.x1 = x1;
			this.y1 = y1;
			this.x2 = x2;
			this.y2 = y2;
			this.x3 = x3;
			this.y3 = y3;
		}
	}
	
	public void getTrueXY(State s){
		int x=cells[0].getX();
		int y=cells[0].getY();
		cells[1].setX(x+s.x1);
		cells[2].setX(x+s.x2);
		cells[3].setX(x+s.x3);
		cells[1].setY(y+s.y1);
		cells[2].setY(y+s.y2);
		cells[3].setY(y+s.y3);
	}
	
	public void rotateLeft(){
		rotateCount--;
		State s=states[rotateCount%states.length];
		getTrueXY(s);
	}
	
	public void rotateRight(){
		rotateCount++;
		State s=states[rotateCount%states.length];
		getTrueXY(s);
	}

七种具体类

举个例子T

T经过旋转,方块0是相对位置是不变的,所以方块0应该是轴心

向左旋转后:

以此类推,T一共有4种旋转形态。图像资源为面板TetrisPanel类中加载的静态图像资源

T:

public class T extends Tetris {
	public T(){
		cells[0]=new Cell(0,4,TetrisPanel.T);
		cells[1]=new Cell(0,3,TetrisPanel.T);
		cells[2]=new Cell(0,5,TetrisPanel.T);
		cells[3]=new Cell(1,4,TetrisPanel.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);
	}
}

I:

public class I extends Tetris {
	public I (){
		cells[0]=new Cell(0,4,TetrisPanel.I);
		cells[1]=new Cell(0,3,TetrisPanel.I);
		cells[2]=new Cell(0,5,TetrisPanel.I);
		cells[3]=new Cell(0,6,TetrisPanel.I);
		states = new State[2];
		states[0] = new State(0,0,0,-1,0,1,0,2);
		states[1] = new State(0,0,-1,0,1,0,2,0);
	}
}

J:

public class J extends Tetris {
	public J (){
		
		cells[0]=new Cell(0,4,TetrisPanel.J);
		cells[1]=new Cell(0,3,TetrisPanel.J);
		cells[2]=new Cell(0,5,TetrisPanel.J);
		cells[3]=new Cell(1,5,TetrisPanel.J);
		states = new State[] { 
				new State(0, 0, 0, -1, 0, 1, 1, 1),
				new State(0, 0, -1, 0, 1, 0, 1, -1),
				new State(0, 0, 0, 1, 0, -1, -1, -1),
				new State(0, 0, 1, 0, -1, 0, -1, 1)};
	}
}

L:

public class L extends Tetris {
	public L (){
		
		cells[0]=new Cell(0,4,TetrisPanel.L);
		cells[1]=new Cell(0,3,TetrisPanel.L);
		cells[2]=new Cell(0,5,TetrisPanel.L);
		cells[3]=new Cell(1,3,TetrisPanel.L);
		states = new State[] { 
				new State(0, 0, 0, -1, 0, 1, 1, -1),
				new State(0, 0, -1, 0, 1, 0, -1, -1),
				new State(0, 0, 0, 1, 0, -1, -1, 1),
				new State(0, 0, 1, 0, -1, 0, 1, 1)};
	}
}

O:

public class O extends Tetris {
	public O (){
		
		cells[0]=new Cell(0,4,TetrisPanel.O);
		cells[1]=new Cell(0,5,TetrisPanel.O);
		cells[2]=new Cell(1,4,TetrisPanel.O);
		cells[3]=new Cell(1,5,TetrisPanel.O);
		states = new State[] { new State(0, 0, 0, 1, 1, 0, 1, 1)};
	}
}

S:

public class S extends Tetris {
	public S (){
		
		cells[0]=new Cell(0,5,TetrisPanel.S);
		cells[1]=new Cell(0,6,TetrisPanel.S);
		cells[2]=new Cell(1,4,TetrisPanel.S);
		cells[3]=new Cell(1,5,TetrisPanel.S);
		states = new State[] { 
				new State(0, 0, 0, 1, 1, -1, 1, 0),
				new State(0, 0, -1, 0, 1, 1, 0, 1)};
	}
}

Z:

public class Z extends Tetris {
	public Z(){
		cells[0]=new Cell(0,4,TetrisPanel.Z);
		cells[1]=new Cell(0,3,TetrisPanel.Z);
		cells[2]=new Cell(1,4,TetrisPanel.Z);
		cells[3]=new Cell(1,5,TetrisPanel.Z);
		states = new State[] { 
		new State(0, 0, -1, -1, -1, 0, 0, 1),
		new State(0, 0, -1, 1, 0, 1, 1, 0)};
	}
}

然后就是在TetrisPanel类中加载7种不同的颜色图像以及游戏背景和游戏结束的图片,TetrisPanel继承于JPanel类

public class TetrisPanel extends JPanel{
	public static BufferedImage T;
	public static BufferedImage O;
	public static BufferedImage L;
	public static BufferedImage J;
	public static BufferedImage S;
	public static BufferedImage Z;
	public static BufferedImage I;
	public static BufferedImage backgeound;
	public static BufferedImage gameover;
	static {
		try {
			T = ImageIO.read(Tetris.class.getResource("T.png"));
			O = ImageIO.read(Tetris.class.getResource("O.png"));
			L = ImageIO.read(Tetris.class.getResource("L.png"));
			J = ImageIO.read(Tetris.class.getResource("J.png"));
			S = ImageIO.read(Tetris.class.getResource("S.png"));
			Z = ImageIO.read(Tetris.class.getResource("Z.png"));
			I = ImageIO.read(Tetris.class.getResource("I.png"));
			backgeound = ImageIO.read(Tetris.class.getResource("tetris.png"));
			gameover = ImageIO.read(Tetris.class.getResource("game-over.png"));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

接下来要考虑游戏中的成员属性:

  • 游戏中应该有正在下落的方块组合和下一个要落的方块组合;
  • 还要有一个Cell的二维数组作为坐标轴;
  • 方块图片(正方形)的边长;
  • 统计游戏中供销处的行数以及计算分数;
  • 游戏状态:正在游戏、暂停、游戏结束,并且要显示在面板中,所以需要在一个String数组中;
  • 当前的游戏状态。
	private Tetrimino currentOne;
	private Tetrimino nextOne;

	private static final int HEIGTH = 20;
	private static final int WEIGHT = 10;
	private static final int CELL_SIZE = 26;
	private Cell[][] wall = new Cell[HEIGTH][WEIGHT];

	private int lines = 0;
	private int score = 0;

	public static int PLAYING = 0;
	public static int PAUSE = 1;
	public static int GAMEOVER = 2;
	public String[] game_states = { "P[pause]", "C[continue]", "S[start]" };

	public int state = 0;

成员属性准备完了,接着是重写JPanel中的paint方法来画坐标轴以及绘制各种元素

	public void paint(Graphics g) {
		//drawImage(BufferedImage image ,x,y,null) 
                //image:要绘制的图片 x:开始绘制的面板的横坐标,y:开始绘制的面板的纵坐标
		g.drawImage(backgeound, 0, 0, null);
		// 平移坐标轴
		g.translate(15, 15);
		// 绘制墙
		paintWall(g);
		// 绘制正在下落的方块组合
		paintCurrentOne(g);
		// 绘制下一个方块组合
		paintNextOne(g);
		// 绘制分数
		paintScores(g);
		// 绘制游戏状态
		paintGameStates(g);
		if(state==GAMEOVER) {
			g.drawImage(gameover, 0, 0, null);
		}
	}

然后来实现这些绘制方法

绘制墙 paintWall

	public void paintWall(Graphics g) {
		for (int i = 0; i < HEIGTH; i++) {
			for (int j = 0; j < WEIGHT; j++) {
				Cell cell = wall[i][j];
				int x = j * CELL_SIZE;
				int y = i * CELL_SIZE;
				if (cell == null) {
				} else {
					g.drawImage(cell.getImage(), x, y, null);
				}
			}
		}
	}

在绘制下落的方块和下一个方块时先通过构造方法来随机获取:

	public TetrisPanel() {
		currentOne = Tetris.randomTetris();
		nextOne = Tetris.randomTetris();
	}
	private void paintCurrentOne(Graphics g) {
		Cell[] cells = currentOne.cells;
		for (Cell c : cells) {
			int x = c.getX() * CELL_SIZE;
			int y = c.getY() * CELL_SIZE;
			g.drawImage(c.getImage(), x, y, null);
		}
	}
	
	private void paintNextOne(Graphics g) {
		Cell[] cells = nextOne.cells;
		for (Cell c : cells) {
			int x = c.getX() * CELL_SIZE;
			int y = c.getY() * CELL_SIZE;
			g.drawImage(c.getImage(), x + 260, y + 30, null);
		}
	}

绘制分数

	public void paintScores(Graphics g) {
		g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 30));
		g.drawString("LINES:" + lines, 295, 165);
		g.drawString("SCORES:" + score, 295, 215);
	}

绘制游戏状态

	private void paintGameStates(Graphics g) {
		g.drawString(game_states[state], 295, 265);
	}

接下来是启动游戏的方法,需要有键盘监听器来指定旋转,快速向下,切换游戏状态的行为:

	public void start() {
		KeyListener l = new KeyAdapter() {
			public void keyPressed(KeyEvent e) {
				int key = e.getKeyCode();
				if (state == PLAYING) {
					if (key == KeyEvent.VK_P) {
						state = PAUSE;
					}
				}
				if(state==PAUSE) {
					if(key==KeyEvent.VK_C) {
						state=PLAYING;
					}
					return;
				}
				if(state==GAMEOVER) {
					if(key==KeyEvent.VK_R) {
						//重置面板上的属性值
						wall=new Cell[HEIGTH][WEIGHT];
						lines=0;
						score=0;
						currentOne = Tetris.randomTetris();
						nextOne = Tetris.randomTetris();
						state=PLAYING;
					}
					return;
				}
				

				switch (key) {
				case KeyEvent.VK_DOWN:
					moveDownAction();
					break;
				case KeyEvent.VK_LEFT:
					moveLeftAction();
					break;
				case KeyEvent.VK_RIGHT:
					moveRightAction();
					break;
				case KeyEvent.VK_UP:
					rotateRightAction();
					break;
				case KeyEvent.VK_SPACE:
					handDownAction();
					break;
				case KeyEvent.VK_Q:
					System.exit(1);
				}
				repaint();// 重新绘制
			}

		};
		// 绑定当前面板 
		this.addKeyListener(l);
		// 当前面板需要焦点
		this.requestFocus();

		for (;;) {
			// 根据等级决定下落的间隔速度来提高游戏难度
			try {
				Thread.sleep(level());
			} catch (Exception e) {
				e.printStackTrace();
			}
			if (state == PLAYING) {
				moveDownAction();
			}
			repaint();
		}
	}

	public int level() {
		int level;
		if(score<50) {
			level = 500;
		}else if(score<100) {
			level = 300;
		}else if(score<150) {
			level = 200;
		}else {
			level = 100;
		}
		return level;
	}

接下来我们要实现以上moveLeftAaction()等绘制方法,注意这与Tetris中的moveLeft()方法是不同的,moveLeft()只是在坐标轴中移动了,而没有在游戏面板中画出来。

	public void landToWall() {
		Cell[] cells = currentOne.cells;
		for (Cell c : cells) {
			int row = c.getX();
			int col = c.getY();
			wall[row][col] = c;
		}
	}

所以在方块组合下落和旋转的过程中,我们需要将下落的方块嵌入墙中,并且要注意:

  • 当方块组合最底下的方块落到第20行时,不能再下落;
  • 判断方块组合是否越界;
  • 判断方块左右两边是否有其他方块,若有则不能移动
    public boolean canDown() {
		Cell[] cells = currentOne.cells;
		for (Cell c : cells) {
			int row = c.getX();
			int col = c.getY();
			if (row >= 19 || wall[row + 1][col] != null) {
				return false;
			}
		}
		return true;
	}
	
	private boolean outOfBounds() {
		Cell[] cells = currentOne.cells;
		for (Cell c : cells) {
			//获取每个方块的列号, 判断是否小于0 或者大于9 就是越界
			int col = c.getX();
			int row = c.getY();
			if (col < 0 || col > 9 || row < 0 || row > 19) {
				return true;
			}
		}
		return false;
	}
	
	private boolean coincide() {
		Cell[] cells = currentOne.cells;
		for (Cell c : cells) {
			int col = c.getX();
			int row = c.getY();
			if (wall[row][col] != null) {
				return true;
			}
		}
		return false;
	}

然后是消行,通过判断行是空或满:

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

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

	private void destroyLine() {
		int line = 0;

		Cell[] cells = currentOne.cells;
		for (Cell c : cells) {
			int row = c.getX();
			if (isFullRow(row)) {
				wall[row] = new Cell[10];
				line++;
			}
		}
		lines += line;
		score += Math.pow(2, line - 1);
		// 行消除后,该下落的下落
		for (int i = 0; i < 20; i++) {
			if (isNullRow(i)) {
				for (int row = i; row > 0; row--) {
					System.arraycopy(wall[row - 1], 0, wall[row], 0, wall[row - 1].length);
				}
			}
		}

	}

然后是键盘监听中的旋转,切换游戏状态,快速下移方法,当然这些移动在要游戏没有结束的情况下(游戏结束即判断nextOne出现的位置上已经有方块了)

	public boolean isGameOver() {
		Cell[] cells = nextOne.cells;
		for(Cell c:cells) {
			int row = c.getX();
			int col = c.getY();
			if(wall[row][col]!=null) {
				return true;
			}
		}
		return false;
	}
	
	public void moveDownAction() {
		if(!isGameOver()) {
			if (canDown()) {
				currentOne.moveDown();
			} else {
				landToWall();
				destroyLine();
				currentOne = nextOne;
				nextOne = Tetris.randomTetris();
			}
		}else {
			state=GAMEOVER;
		}
		
	}

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

	private void moveRightAction() {
		currentOne.moveRight();
		if (outOfBounds() || coincide()) {
			currentOne.moveLeft();
		}
	}

        //手动按↓键即再次向下移动
	public void handDownAction() {
		while(canDown()) {
			currentOne.moveDown();
		}
	}

        //也可以向左旋转
	public void rotateRightAction() {
		currentOne.rotateRight();
		if (outOfBounds() || coincide()) {
			currentOne.rotateLeft();
		}
	}

最后就是main方法了:

	public static void main(String[] args) {
		// 创建窗口对象
		JFrame frame = new JFrame("俄罗斯方块");
		// 设置窗口大小
		frame.setSize(535, 580);
		// 创建面板对象
		TetrisPanel tp = new TetrisPanel();
		// 将面板嵌入窗口中
		frame.add(tp);
		// 设置窗口可见性
		frame.setVisible(true);
		// 设置窗口关闭是程序终止
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		// 设置窗口居中
		frame.setLocationRelativeTo(null);
		tp.start();
	}

完整项目代码:https://download.csdn.net/download/qq_39192827/10943352

猜你喜欢

转载自blog.csdn.net/qq_39192827/article/details/86688710