首先7种俄罗斯方块都是由4块小方块组成的,所以我们可以抽象出一个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类的一个属性
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();
}
}
}
接下来要考虑游戏中的成员属性:
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;
}
}
所以在方块组合下落和旋转的过程中,我们需要将下落的方块嵌入墙中,并且要注意:
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();
}