在上一篇里,俄罗斯方块实现了简单的向下、左、右移动,在这篇文章中会继续实现旋转和消行、设置快速下落、显示分数和所消行数、暂停、重新开始,以及游戏结束的实现。
在上一篇中,有一个图片标有各个图形各个位置的具体的编号
旋转也是基于0 1 2 3的编号。在旋转的过程中,以标号为0的方块为轴,绕着他进行旋转,记录下每次旋转时,0、1、2、3块的新坐标。在每个图形的类中(即T、S、O类等),增设一个state数组,来记录每个图形存在的四种状态的坐标(其中I、S、Z型有两种状态,O型有一种状态)。
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]; //图形不同数组大小也不同
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()则是为了在到达底部或者出界时,将已经错误旋转的图形转回来,这种方法与之前文章中左移右移导致出界的解决办法相同。
这些准备工作结束后,我们就在Tetris类中继续加入游戏逻辑。
case KeyEvent.VK_UP:
rotateRightAction();
break;
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();
}
}