Java俄罗斯方块目录:
在第一篇中,已经把游戏场景和基本游戏元素都绘制出来了,接下来我们编写游戏的操作。
在俄罗斯方块中,有以下几种操作方式:
在以上操作方式当中,都需要判断四格方块是否能够做出对于操作,比如,到墙底或下面有方块时不能下落,左边有方块或墙时不能左移等,所以在每次进行操作之前都要判断是否能执行此次操作,否则会出现数组下标越界异常。当方块不能下落时,应该让方块嵌入墙中,如果能填满行时,还要进行消行并且移动消除行上方的行同时计分,同时让下一个将要出现的方块出现,当下一个将要出现的方块出现的区域有方块时,游戏结束。
根据这段游戏逻辑,抽象出以下方法:
1. outOfBounds判断方块是否出界。
public boolean outOfBounds() {
Cell[] cells = currentOne.cells;
for(Cell c:cells) {
int col = c.getCol();
int row = c.getRow();
if(col<0||col>9||row>19||row<0)
return true;
}
return false;
}
2. coincide,判断方块是否重合,即操作时旁边是否有方块。
public boolean coincide() {
Cell[] cells = currentOne.cells;
for(Cell c:cells) {
int row = c.getRow();
int col = c.getCol();
if(wall[row][col]!=null)
return true;
}
return false;
}
3. moveRightAction,按一个键使四格方块右移一格。
public void moveRightAction() {
currentOne.moveRight();
if(outOfBounds()||coincide())
currentOne.moveLeft();
}
4. moveLeftAction,按一个键使四格方块左移一格。
public void moveLeftAction() {
currentOne.moveLeft();
if(outOfBounds()||coincide()) {
currentOne.moveRight();
}
}
5. 四格方块变形。
让四格方块变形需要先记录每个小方块在不同形状时相对于第一个小方块即序号为 0 的小方块的相对位置。设置一个计数器,同时为了方便取得每个时期四格方块的状态,只需要用计数器对当前状态取余即可。不同形状的四格方块可旋转的次数不同,即四格方块旋转多少次能变回最开始的形状。有的能旋转4次,有的能旋转2次或0次,比如I,只能旋转2次,O,没有旋转的意义,即0次,其余的形状大都能旋转4次。
首先,在Tetromino类中添加旋转状态属性和计数器,计数器的取值尽量大并且要是一个能被4整除的数,因为旋转次数最多为4次。
/*
* 旋转状态属性,状态个数以数组的形式进行存储
*/
protected State[] states;//不同形状四格方块有效旋转次数不同,不用赋初值。
/*定义一个变量:充当旋转次数的计数器*/
private int count = 100000;
定义一个内部类,用于存储四格方块的相对位置,按照国际惯例,创建相应的构造器,get/set方法并重写toString方法。
/*
* 定义内部类:State,用于
* 封装每次旋转后的相对于轴的其他三个方块的
* 坐标(行号,列号)
*/
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) {
this.row0 = row0;
this.col0 = col0;
this.row1 = row1;
this.col1 = col1;
this.row2 = row2;
this.col2 = col2;
this.row3 = row3;
this.col3 = col3;
}
public int getRow0() {
return row0;
}
public void setRow0(int row0) {
this.row0 = row0;
}
public int getCol0() {
return col0;
}
public void setCol0(int col0) {
this.col0 = col0;
}
public int getRow1() {
return row1;
}
public void setRow1(int row1) {
this.row1 = row1;
}
public int getCol1() {
return col1;
}
public void setCol1(int col1) {
this.col1 = col1;
}
public int getRow2() {
return row2;
}
public void setRow2(int row2) {
this.row2 = row2;
}
public int getCol2() {
return col2;
}
public void setCol2(int col2) {
this.col2 = col2;
}
public int getRow3() {
return row3;
}
public void setRow3(int row3) {
this.row3 = row3;
}
public int getCol3() {
return col3;
}
public void setCol3(int col3) {
this.col3 = col3;
}
@Override
public String toString() {
return "State [row0=" + row0 + ", col0=" + col0 + ", row1=" + row1 + ", col1=" + col1 + ", row2=" + row2
+ ", col2=" + col2 + ", row3=" + row3 + ", col3=" + col3 + "]";
}
}
在7个形状类中初始化不同状态时的相对坐标。
(1)I,共有两种旋转状态,竖着和横着,修改I类的构造器为。
public I() {
cells[0]=new Cell(0,4,Tetris.I);
cells[1]=new Cell(0,3,Tetris.I);
cells[2]=new Cell(0,5,Tetris.I);
cells[3]=new Cell(0,6,Tetris.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);
}
(2)L,共有四种旋转状态,修改L类的构造器为。
public L() {
cells[0]=new Cell(0,4,Tetris.L);
cells[1]=new Cell(0,3,Tetris.L);
cells[2]=new Cell(0,5,Tetris.L);
cells[3]=new Cell(1,3,Tetris.L);
states = new State[4];
states[0] = new State(0,0,0,-1,0,1,1,-1);
states[1] = new State(0,0,-1,0,1,0,-1,-1);
states[2] = new State(0,0,0,1,0,-1,-1,1);
states[3] = new State(0,0,1,0,-1,0,1,1);
}
(3)其他几个形状类也类似,根据第一章给的坐标图,发挥一下想象力,把各个旋转状态的坐标初始化好即可。
初始化完毕以后,回到Tetromino类中创建旋转方法:
(1)rotateRight,顺时针旋转四格方块,用于旋转方块。
/*
* 顺时针,向右旋转四格方块
*/
public void rotateRight() {
//旋转一次,计数器自增长1
count++;
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);
}
(2)rotateLeft,逆时针旋转四格方块,当方块不能旋转时使方块转回去。
/*
* 逆时针,向左旋转四格方块
*/
public void rotateLeft() {
//旋转一次,计数器自减1
count--;
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);
}
最后,回到Tetris类中,创建顺时针旋转方法,rotateRightAction。
public void rotateRightAction() {
currentOne.rotateRight();
if(outOfBounds()||coincide()){
currentOne.rotateLeft();
}
}
6. isGameOver,判断游戏是否结束,判断下一个将要出现的方块出现的位置是否有方块。
public 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;
}
7. destroyLine,消行并计分,当四个方块嵌入墙中时,使得行满,便消除满行并使消除行上分的方块下落对应行数并计分。
(1)首先创建一个判断当前行是否已满的方法isFullLine。
public boolean isFullLine(int row) {
//取出当前行的所有列
Cell[] line = wall[row];
for(Cell r:line) {
if(r==null) {
return false;
}
}
return true;
}
(2)然后在创建destroyLine方法。
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;
}
8. canDrop,判断四格方块能否下落。
public boolean canDrop() {
Cell[] cells = currentOne.cells;
/*
*
*/
for(Cell c:cells) {
/* 获取每个元素的行号,列号
* 判断:
* 只要有一个元素的下一行上有方块
* 或者只要有一个元素到达最后一行
* 就不能再下落了
*/
int row = c.getRow();
int col = c.getCol();
if (row==19) {//判断是否到达底部
return false;
}
else if(wall[row+1][col]!=null) {//判断下方是否有方块
return false;
}
}
return true;
}
9. softDropAction,按一个键使四格方块下落一格,判断能否下落,不能下落便嵌入墙中,判断能否消行并判断游戏是否结束,没结束便继续生成四格方块,否则给出游戏结束提示。
public void softDropAction() {
if(canDrop()) {
currentOne.softDrop();
}
else {
landToWall();
destroyLine();
if(!isGameOver()) {
currentOne = nextOne;
nextOne = Tetromino.randomOne();
}
else {
game_state = GAMEOVER;
}
}
}
10. handDropAction,瞬间下落,按一个键使四格方块瞬间到底,原理同缓慢下落。
public void handDropAction() {
for(;;) {
if(canDrop()){
currentOne.softDrop();
}
else
break;
}
landToWall();
destroyLine();
if(!isGameOver()) {
currentOne = nextOne;
nextOne = Tetromino.randomOne();
}
else {
game_state = GAMEOVER;
}
}
以上主要方法创建完毕以后,接下来创建一个start方法,用于调用游戏操作逻辑并监听键盘和描述游戏主要逻辑。
(1)开启键盘监听事件,根据获取到的按键调用相应的方法做出对应的动作,其中,很重要的一点是必须要把俄罗斯方块这个窗口设为焦点,否则监听无效,也就是这两行代码。
this.addKeyListener(l);
this.requestFocus();
以下便是监听事件的代码,通过重写keyPressed方法并用匿名内部类的形式来实现对按键的响应。
//开启键盘监听事件
KeyListener l = new KeyAdapter() {
/*
* KeyPressed()
* 是键盘按钮 按下去所调用的方法
*/
@Override
public void keyPressed(KeyEvent e) {
// 获取一下键子的代号
int code = e.getKeyCode();
if(code == KeyEvent.VK_P) {
if(game_state == PLAYING)
game_state=PAUSE;
}
if(code == KeyEvent.VK_C) {
if(game_state == PAUSE)
game_state=PLAYING;
}
if(code == KeyEvent.VK_S) {
game_state=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:
handDropAction();
break;
}
//按一次重新绘制一次
repaint();
}
};
//面板添加监听事件对象
this.addKeyListener(l);
//面板对象设置成焦点
this.requestFocus();
(2)添加一个四格方块自动下落的逻辑,当处于游戏中时,四格方块将每隔0.4s下落一次(下落速度由Thread.sleep(400)控制,可修改括号内数值自行调速,单位为ms),直到不能下落,便将四格方块嵌入墙中,并判断消行和游戏是否结束,每次下落都重绘一次游戏画面。
while(true) {
if(game_state == PLAYING) {
/*
* 当程序运行到此,会进入睡眠状态,
* 睡眠时间为400毫秒,单位为毫秒
* 400毫秒后,会自动执行后续代码
*/
try {
Thread.sleep(400);
} catch (InterruptedException e) {
// 抓取打断异常
e.printStackTrace();
}
if(canDrop()) {
currentOne.softDrop();
}
else {
landToWall();
destroyLine();
//将下一个下落的四格方块赋值给正在下落的变量
if(!isGameOver()) {
currentOne = nextOne;
nextOne = Tetromino.randomOne();
}
else {
game_state = GAMEOVER;
}
}
/*每次下落之后都要重新绘制一次来刷新界面*/
repaint();
}
(3)完整的start方法。
public void start() {
game_state = PLAYING;
//开启键盘监听事件
KeyListener l = new KeyAdapter() {
/*
* KeyPressed()
* 是键盘按钮 按下去所调用的方法
*/
@Override
public void keyPressed(KeyEvent e) {
// 获取一下键子的代号
int code = e.getKeyCode();
if(code == KeyEvent.VK_P) {
if(game_state == PLAYING)
game_state=PAUSE;
}
if(code == KeyEvent.VK_C) {
if(game_state == PAUSE)
game_state=PLAYING;
}
if(code == KeyEvent.VK_S) {
game_state=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:
handDropAction();
break;
case KeyEvent.VK_B:
boom();
break;
default:
break;
}
//按一次重新绘制一次
repaint();
}
};
//面板添加监听事件对象
this.addKeyListener(l);
//面板对象设置成焦点
this.requestFocus();
while(true) {
if(game_state == PLAYING) {
/*
* 当程序运行到此,会进入睡眠状态,
* 睡眠时间为300毫秒,单位为毫秒
* 300毫秒后,会自动执行后续代码
*/
try {
Thread.sleep(400);
} catch (InterruptedException e) {
// 抓取打断异常
e.printStackTrace();
}
if(canDrop()) {
currentOne.softDrop();
}
else {
landToWall();
destroyLine();
//将下一个下落的四格方块赋值给正在下落的变量
if(!isGameOver()) {
currentOne = nextOne;
nextOne = Tetromino.randomOne();
}
else {
game_state = GAMEOVER;
}
}
/*
* 下落之后,要重新进行绘制,才会看到下落后的位置
* repaint方法,也是JPanel类中提供的
* 此方法中调用了paint方法
*/
}
repaint();
}
}
最后将start方法添加到main方法中,就可以愉快的玩俄罗斯方块了。
(1)完整main方法。
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, 595);
//4:设置窗口居中
frame.setLocationRelativeTo(null);
//5:设置窗口关闭,即程序中止
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//游戏的主要逻辑封装在start方法中
panel.start();
}