前言:
写俄罗斯方块需要一些材料,以下为百度云链接,点击下载(若链接失效,请联系我)
俄罗斯方块需要的材料链接
正文:
第一步:
分析游戏界面(以下界面并不完成,只是一部分),通过游戏界面,抽象出几种类型
从图中我们可以看出有一个表格(就是界限,我们称之为墙,在此例子中墙的大小为20行10列)和元胞(就是一个方格)。
俄罗斯方块下落的图案有七种类型,还会显示下一个图案的类型。这七种类型类似于字母T、I、O、J、L、S、Z,一种类型对应一种颜色。(意思就是T类型的就用黄色的四个元胞表示,O类型用红色的四个元胞表示,以下图片在资源包中含有)
然后每个类型都可以向左移动,向右移动和向下移动。
第二步:
定义类型
共同特征:行号、列号,图片
共同行为:向左、向右、向下移动、提供JavaBean相关的规范
//Cell.java
package com.tetris;
/**
* 俄罗斯方块中的最小单位:一个方格
* 特征:
* row---行号
* col---列号
* image---对应的图片
* 行为:
* 向左移动---left()
* 向右移动---right()
* 下落---drop()
* @author DELL
*
*/
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() {
// TODO Auto-generated method stub
return "[row="+row+",col="+col+"]";
}
public void left() {
col--;
}
public void right() {
col++;
}
public void drop() {
row++;
}
}
因为每种类型都是由四个元胞组成的,每种类型都可以向左、向右、向下移动,所以我们可以提取一个父类,我们取名为Tetromino
2 . Tetromino类,是所有七种类型的父类
共同特征:cells-->四个元胞(用数组表示)-->权限修饰词protected(方便之后七种类型继承时可以 获取)
共同行为:向左、向右、向下移动、提供JavaBean相关的规范
额外的方法:在俄罗斯方块中,我们会随机获得一种类型的图案,为此,我们需要写一个方法来随机获得一个类型。
//Tetromino.java
package com.tetris;
import java.util.Arrays;
/**
* 四格方块
* 属性:
* cells---四个方块
* 行为:
* moveLeft()---左移动
* moveRight()---右移动
* softDrop()---软下落
* @author DELL
*
*/
public class Tetromino {
protected Cell[] cells=new Cell[4];
public Cell[] getCells() {
return cells;
}
public void setCells(Cell[] cells) {
this.cells = cells;
}
public void moveLeft() {
for (int i = 0; i < cells.length; i++) {
cells[i].left();
}
}
public void moveRight() {
for (int i = 0; i < cells.length; i++) {
cells[i].right();
}
}
public void softDrop() {
for (int i = 0; i < cells.length; i++) {
cells[i].drop();
}
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "["+Arrays.toString(cells)+"]";
}
/*
* 随机生成一个四格方块
*/
public static Tetromino randomOne() {
Tetromino t=null;
int n=(int)(Math.random()*7);
switch(n) {
case 0:t=new T();break;//获取T类型,以下依次类推
case 1:t=new I();break;
case 2:t=new O();break;
case 3:t=new S();break;
case 4:t=new Z();break;
case 5:t=new L();break;
case 6:t=new J();break;
}
return t;
}
}
3 . T、I、O、L、J、S、Z类,是七种图案,继承Tetromino类
因为此例子中我们是10行,所以初始化时我们行号和列号的复制如图所示(从左到右,从上到下,依次为I、S、T、Z、L、O、J型)
(cells数组的存放需按照图上的顺序来,因为之后我们会旋转图形,需以cells[0]为轴心进行旋转)
以T类为例子,其它六种类可以模仿依次写完
//T.java
package com.tetris;
public class T extends Tetromino {
/*
* 提供构造器,进行初始化
* T型的四格方块的位置
*/
public T() {
cells[0]=new Cell(0,4,Tetris.T);//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);
}
}
第三步:
在主类也就是Tetris.java中 加载静态资源,这里的静态资源有资源包中的七种类型的元胞图片和背景图片
//Tetris.java
//以下内容直接写在Tetris类中
public static BufferedImage T;
public static BufferedImage I;
public static BufferedImage O;
public static BufferedImage S;
public static BufferedImage Z;
public static BufferedImage L;
public static BufferedImage J;
public static BufferedImage background;
static {
/*
* getResource(String url)
* url:加载图片的路径
* 相对位置是同包下
*/
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"));
S=ImageIO.read(Tetris.class.getResource("S.png"));
Z=ImageIO.read(Tetris.class.getResource("Z.png"));
L=ImageIO.read(Tetris.class.getResource("L.png"));
J=ImageIO.read(Tetris.class.getResource("J.png"));
background=ImageIO.read(Tetris.class.getResource("tetris.png"));
} catch (Exception e) {
e.printStackTrace();
}
}
第四步:
在Tetris类中写main方法,在main方法中写一个窗口,并设置一些窗口的属性,并且让这个Tetris类继承JPanel,即这个主类是以一块面板,从而嵌入窗口Jframe。
//Tetris.java中main方法中的一部分内容
//别忘记让Tetris继承JPanel
//创建窗口
JFrame frame=new JFrame("俄罗斯方块");
//设置窗口的可见性
frame.setVisible(true);
//设置窗口的大小
frame.setSize(535, 580);
frame.setLocationRelativeTo(null);//窗口居中
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//窗口关闭即程序结束
//创建游戏界面,即面板
Tetris panel=new Tetris();
frame.add(panel);
第五步:
面板上自带一个画笔,有一个功能:自动绘制--->JPanel里的paint()
因此我们重写JPanel中的paint方法,此方法中我们需要绘画出背景图片、绘制墙(也就是20x10的表格)、绘制正在下落的四个方块和下一个下落的方块。
由此引出三个额外需要写的三个方法和若干个属性:
方法:paintWall(Graphics)、paintCurrentOne(Graphics)、paintNextOne(Graphics)
属性:正在下落的四格方块 :currentOne
下一个下落的四格方块 :nextOne
墙:20行10列的表格 :Cell[][] wall=new Cell[20][10]
每个元胞的宽度:private static final int CELL_SIZE=26
还有一个问题就是我们的开始绘制时的原点在最左上角(向右x轴无穷大,向下y轴无穷大),考虑到背景图片的问题,所以我们需要将坐标轴平移15个像素。
//Tetris类中
//属性:正在下落的四格方块
private 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;
/*
* 重写JPanel中的paint(Graphics g)
*/
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);
}
/*
* 绘制下一个将要下落的四个方块,
* 绘制到面板的右上角的相应位置
*/
public void paintNextOne(Graphics g) {
//获取nextone的四格方块
Cell[] cells=nextOne.cells;
for (Cell cell : cells) {
//获取每个元素的行号和列号
int row=cell.getRow();
int col=cell.getCol();
//求出对应的横坐标和纵坐标
int x=col*CELL_SIZE+260;
int y=row*CELL_SIZE+26;
//在面板中绘画出来
g.drawImage(cell.getImage(), x, y, null);
}
}
/*
* 绘制正在下落的四格方块
* 取出数组的元素
* 绘制元素的图片
* 横坐标x
* 纵坐标y
*/
public void paintCurrentOne(Graphics g) {
Cell[] cells =currentOne.cells;
for (Cell cell : cells) {
int x=cell.getCol()*CELL_SIZE;
int y=cell.getRow()*CELL_SIZE;
g.drawImage(cell.getImage(), x, y, null);
}
}
//绘制墙的函数:20行10列的表格,是个二维数组,使用双层循环。
public void paintWall(Graphics g) {
for (int i = 0; i < 20; i++) {
for(int j=0;j<10;j++) {
int x=CELL_SIZE*j;
int y=CELL_SIZE*i;
//如果墙中的位置为空,那么就画一个小格子,否则将图案画出来
Cell cell=wall[i][j];
if(cell==null) {
g.drawRect(x, y, CELL_SIZE, CELL_SIZE);
}else{
g.drawImage(cell.getImage(), x, y, null);
}
}
}
}
第六步:
基本上我们框架已经搭好,我们现在要做的就是让俄罗斯方块可以向左向右和向下动起来。我们在主类Tetris中定义一个start()方法,并且在main方法中进行调用。
start()方法将用来封装游戏的主要逻辑
1 . 一个死循环,让currentOne每0.3秒往下下落一格,如果可以继续往下下落,则继续下落,否则就将currentOne嵌入到墙内并绘制出来,并且进行下一个图案的下落。
2 . 添加键盘监听,当按下↓时进行下落,按下←时向左移,按下→时向右移。(new 一个键盘监听适配器,并且重写keyPressed方法)
以上两个功能引出以下方法:
判断是否可以下落:canDrop()
将图案嵌入墙内:landToWall()
执行下落步骤:softDrop()
执行向左移动步骤:moveLeftAction()
执行向右移动步骤:moveRightAction()
判断是否越界(左右越界,即列号越界):outOfBounds()
判断是否重合(在不越界的基础上进行判断):coincide()
//Tetris类中
/*
* 封装了游戏的主要逻辑
*/
public void start() {
//开启键盘监听
KeyListener keylistener=new KeyAdapter() {
@Override
public void keyPressed(KeyEvent arg0) {
int code=arg0.getKeyCode();
switch (code) {
case KeyEvent.VK_DOWN:
softDropAction();
break;
case KeyEvent.VK_LEFT:
moveLeftAction();
break;
case KeyEvent.VK_RIGHT:
moveRightAction();
break;
}
repaint();
}
};
//添加键盘监听
this.addKeyListener(keylistener);
//获得焦点
this.requestFocus();
while (true) {
/*
*当程序运行到此,会进入睡眠状态
*睡眠时间为200毫秒,单位为毫秒
*300毫秒之后,会自动执行后续代码
*/
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (canDrop()) {
currentOne.softDrop();
}else{
landToWall();
//将下一个下落的四个方块复制到正在下落的方格
currentOne=nextOne;
nextOne=Tetromino.randomOne();
}
/*
* 下落之后,需要重新绘制,才会看到下落后的位置
* repaint方法也是JPanel类中提供的
* 此方法中调用paint方法
*/
repaint();
}
}
/*
* 执行下落步骤
*/
protected void softDropAction() {
if(canDrop()) {
currentOne.softDrop();
}else {
landToWall();
//将下一个下落的四个方块复制到正在下落的方格
currentOne=nextOne;
nextOne=Tetromino.randomOne();
}//else这块是为了完美体验度
}
protected void moveLeftAction() {
currentOne.moveLeft();
if(outOfBounds()||coincide()) {
currentOne.moveRight();
}
}
protected void moveRightAction() {
currentOne.moveRight();
if(outOfBounds()||coincide()) {
currentOne.moveLeft();
}
}
//越界
public boolean outOfBounds() {
Cell[] cells=currentOne.cells;
for (Cell cell : cells) {
int col=cell.getCol();
if(col<0||col>9||row>19||row<0) {
return true;
}
}
return false;
}
//重合
public 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;
}
/*
* 判断是否可以下落
*/
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;
}
}
第七步:
我们往往会觉得下落太慢,我们可以添加一个监听空格键进行调用handDropAction(),利用死循环让其直接下落到最下面
方法代码如下
/*
* 快速下落
*/
public void handDropAction() {
for(;;) {
if(canDrop()) {
currentOne.softDrop();
}else {
break;
}
}
landToWall();
destroyLine();//消行,此方法我们还没有讲到,之后在第九步会讲。
//将下一个下落的四个方块复制到正在下落的方格
currentOne=nextOne;
nextOne=Tetromino.randomOne();
}
第八步:
现在我们可以向下左右移动,可以看出来我们还缺一个旋转的操作。之前我们有提供过每种类型的四个元胞的位置(参考第二步定义类型中的第三小块),其中cells[0]就是旋转的轴心,我们可以顺时针(向右旋转)和逆时针(向左旋转)进行旋转。其次旋转之后的状态需要保存下来,所以先在七种类型的父类即Tetromino中定义一个内部类叫State,这个类有四对行号和列号,用来存放旋转之后每个点的相对坐标。
Tetromino中的内部类State:
属性: 设计八个属性,分别存储四哥方块的相对位置
int row0,col0,row1,col1,row2,col2,row3,col3;
方法:getter和setter方法
无参构造函数
八个参数的构造函数
内部类定义完之后我们可以在Tetromino中加一个属性State[] states;用来给七种类型的子类继承以此各自保存旋转后需要的状态(比如I型相当于只有两个状态,O型只有一个,Z和S型两个状态,其他的都是四种状态)
在父类中有了这个states属性之后,我们就可以在七种类型的类中对这个states进行初始化。下面我举个例子,剩下的可以尝试自己写。
T型:状态的行号和列号都是相对于cells[0]而言,所以cells[0]是原点。
我们的states初始化如下:
//T.java中在无参构造函数中添加
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);
我们还需要在Tetromino类中添加向左旋转rotateLeft()和向右旋转rotateRight()的方法,为了方便得到状态,我们还要在Tetromino类中定义一个变量:充当旋转次数的计数器count=100000,向右旋转,计数器加一,向左旋转,计数器减一。旋转后的状态通过count对状态的个数进行取余,最后根据状态的相对位置更改cells的行号和列号。
下面是rotateRight()的代码,rotateLeft()可以自己类推出来
public void rotateRight() {
//旋转一次,计数器自增1
count++;
State state=states[count%states.length];
Cell cell=cells[0];
int row=cell.getRow();
int col=cell.getCol();
cells[1].setRow(row+state.row1);
cells[1].setCol(col+state.col1);
cells[2].setRow(row+state.row2);
cells[2].setCol(col+state.col2);
cells[3].setRow(row+state.row3);
cells[3].setCol(col+state.col3);
}
现在我们拥有了类型旋转的函数,我们可以开始写在界面上进行旋转的函数了。我们是按↑键进行旋转。于是在Tetris中的start方法中添加监听up键并且调用rotateRightAction()方法
在rotateRightAction()中,我们需要让当前下落的俄罗斯方块进行顺时针旋转,如果越界或者重合(前面已经写过了)了,我们需要将其逆时针旋转一次。代码如下:
public void rotateRightAction() {
currentOne.rotateRight();
if(outOfBounds()||coincide()) {
currentOne.rotateLeft();
}
}
第九步:
我们的图案可以正常的移动和旋转了,我们就需要写消行的代码了,即destroyLine()方法。消行是发生在当前下落的俄罗斯方块嵌入墙后发生的。我们需要判断当前下落的俄罗斯方块的每个元胞所在的这一行及以下的都要遍历一遍看是否有哪一行满了,如果满了,我们需要将这一行等于它的上一行(相当于整体往下移)。
代码如下:
/*
* 消行,忙一行,上面的方块向下平移
*/
public void destroyLine() {
//取出已经嵌入墙内的图案的行号和列号
Cell[] cells=currentOne.cells;
int lines=0;
for (Cell cell : cells) {
int row=cell.getRow();
while(row<20) {
if(isFullLine(row)) {
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];
lines++;
}
row++;
}
}
totalScore+=scores_pool[lines];
totalLine+=lines;
}
/*
* 判断是否满行
*/
public boolean isFullLine(int row) {
Cell[] line=wall[row];
for (Cell cell2 : line) {
if(cell2==null) {
return false;
}
}
return true;
}
第十步:
最后就是什么时候结束和按键可以暂停和重来
游戏结束就是当下一个要下落的俄罗斯方块无法嵌入墙的时候游戏结束,代码如下:
public boolean isGameOver() {
Cell[] cells=nextOne.cells;
for (Cell cell : cells) {
int row=cell.getRow();
int col=cell.getCol();
if(wall[row][col]!=null) {
return true;
}
}
return false;
}
而游戏的暂停和继续的设计,需要我们进行记录游戏的状态。在游戏的最开始,游戏状态为PLAYING,当按下P键,游戏状态为PAUSE,按下C键游戏继续,游戏状态为PLAYING,如果按下S键,重新来,游戏状态为PLAYING,并且要将墙清空。如果游戏结束,状态为GAMEOVER。
======================================================================
最后的几步可能讲的不清楚,如果想知道请评论我,我会回复你的。
需要源码的也请评论我~~