相信俄罗斯方块大家都玩过,在这里就不多介绍规则了,用到的主要框架是Swing。
Java俄罗斯方块目录:
以下是要用到的素材:
1.小方块
2.游戏背景图
3.GameOver
——————————————————————我是分割线—————————————————————
好了,话不多说,我们直接进入正题:
直接上图:
上图展示的是俄罗斯方块里的七种经典方块,每个方块都是由4个小方块组成,序号是为了方块能够变形而特意做的标记。
抽象出对应的数据类型
首先,先创建一个Cell类,用来表示一个小方块,Cell类的主要成员就是这些。
- row,表示小方块的行号。
- col,表示小方块的列号。
- image,表示小方块的图片,就是之前素材里的。
- left(),right(),drop(),分别表示一个小方块的左移一格,右移一格,下降一格。
public class Cell {
private int row;
private int col;
private BufferedImage image;
public Cell() {}
public Cell(int row, int col, BufferedImage image) {
this.row = row;
this.col = col;
this.image = image;
}
/*向左移动*/
public void left() {
col--;
}
/*向右移动*/
public void right() {
col++;
}
/*向下移动*/
public void drop() {
row++;
}
}
接下来,按照国际惯例(JavaBean规范),我们把这个类补全了,创建全参合无参构造器,属性的get/set方法并重写toString方法。
public class Cell {
private int row;
private int col;
private BufferedImage image;
@Override
public String toString() {
return "(" + row + ", " + col + ")";
}
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;
}
public Cell() {}
public Cell(int row, int col, BufferedImage image) {
this.row = row;
this.col = col;
this.image = image;
}
/*向左移动*/
public void left() {
col--;
}
/*向右移动*/
public void right() {
col++;
}
/*向下移动*/
public void drop() {
row++;
}
}
之前说过,俄罗斯方块里面有七个经典形状,他们有一些共同特征:
- 都是由4个小方块组成。
- 都能左移,右移,下落。
- 变形,因为变形比较麻烦,就不写在父类里了,后面再介绍变形的方法。
那么现在我们就创建一个Tetromino类来作为7个经典形状的父类,并提供相应的成员。
- Cell数组,用于创建4个小方块。
- moveLeft(),moveRight(),softDrop(),分别用于四格方块的左移,右移和软下落,软下落也就是四格方块下落一个,以后会写一个硬下落,让四格方块瞬间落下。
public class Tetromino {
protected Cell[] cells=new Cell[4];
/*四格方块向左移动*/
public void moveLeft() {
for(Cell c:cells)
c.left();
}
/*四格方块向右移动*/
public void moveRight() {
for(Cell c:cells)
c.right();
}
/*四格方块向下移动*/
public void softDrop() {
for(Cell c:cells)
c.drop();
}
@Override
public String toString() {
return "[" + Arrays.toString(cells) + "]";
}
}
接着,在创建7个不同的形状,根据形状的大致模样,为了方便,这里就用I,J,L,O,S,T,Z来表示了。
形状都要继承Tetromino类,这7个形状类的作用就是为了初始化形状的位置,在初始化位置之前,提一下,游戏的资源,也就是背景,图片等为了加载的效率,一般都创建为静态成员,所以这里用将图片创建为静态的并用静态代码块来调用ImageIO流来读取图片,在主类当中先创建好,方便以后的调用,所以,在初始化形状之前,先创建一个主类,Tetris类,因为这次主要是用JPanel这个框架来完成制作,所以Tetris需要继承JPanel,并通过重写JPanel的方法来完成游戏的制作。
Tetris类,先初始化游戏资源,主要是用BufferedImage和ImageIO流来完成,因为IO流有一个检查型的异常,所以这里需要用try把IO流给圈起来,并用catch来捕获异常。
public class Tetris extends JPanel{
//载入方块图片
public static BufferedImage 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 gameover;
static {
try {
/*
* getResource(String url)
* url:加载图片的路径
* 相对位置是同包下
*/
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"));
gameover = ImageIO.read(Tetris.class.getResource("game-over.png"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
接下来,在用对应的形状类来初始化形状。
public class I extends Tetromino{
/*
* 提供构造器进行初始化
* 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);
}
}
剩下的几个形状也类似,只需要根据前面的形状坐标来修改即可,图片随意,创建好形状之后,需要生成一个四格方块,所以回到Tetromino类,为了方便以后调用,写一个随机生成方块的静态方法。
一共有7个形状,这里用(int)Math.random()*7来表示7个不同的形状,因为形状都是继承与父类,在这里直接向上转型就可以了。
/*随机生成一个四格方块*/
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;
}
都创建好了以后,回到主类Tetris类当中,在游戏当中,有以下这些对象,我们把他们抽象成相应的成员:
- currentOne,描述正在下落的方块。
- nextOne,描述将要下落的方块。
- wall,游戏的主区域。
- 这里需要提一下,生成方块的方法,我们放到父类Tetromino当中,为了方便调用,我们把生成方块的方法创建为静态方法。
/*属性:正在下落的四格方块*/
private Tetromino currentOne = Tetromino.randomOne();
/*属性:将要下落的四格方块*/
private Tetromino nextOne = Tetromino.randomOne();
/*属性:墙,20行 10列的 表格 宽度为26*/
private Cell[][] wall=new Cell[20][10];
接下来,在Tetris中创建一个main方法,在main方法中创建游戏场景,窗口的尺寸为了和游戏场景相符,用535*595的大小。
public static void main(String[] args) {
//1:创建一个窗口对象
JFrame frame=new JFrame("玩玩俄罗斯方块");
//2:设置为可见
frame.setVisible(true);
//3:设置窗口的尺寸
frame.setSize(535, 595);
//4:设置窗口居中
frame.setLocationRelativeTo(null);
//5:设置窗口关闭,即程序中止
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
运行效果图
一. 绘制游戏背景
现在,我们有了游戏的窗口,接下来要做的就是绘制游戏场景,向main方法中添加以下两行代码,为了避免一些不必要的麻烦,建议把这两行代码加到 JFrame frame=new JFrame("玩玩俄罗斯方块"); 这行代码下面。
//创建游戏界面,即画板(面板)
Tetris panel = new Tetris();
//将面板嵌入窗口
frame.add(panel);
接下来,让我们来绘制游戏吧,重写JPanel当中的paint方法(paint方法用来描述游戏的所有场景和元素),绘制游戏背景,在这里用JPanel框架绘制主要用到以下方法。
- drawImage(image,x,y,null),用于绘制图片。
- drawRect(x,y,width,height),用于绘制图形。
- drawString(str,x,y),用于绘制字符串。
- 以上三个方法都需要通过画笔Graphics来调用,参数的含义就不多说了。
public void paint(Graphics g) {
//绘制背景
/*
* g:画笔
* g.drawImage(image,x,y,null)
* image:绘制的图片
* x:开始绘制的横坐标
* y:开始绘制的纵坐标
*/
g.drawImage(background, 0,0, null);
}
游戏背景我们绘制出来了,接下来继续绘制其他游戏元素。
1.paintWall,绘制游戏主区域,编写以下方法,在paint方法中调用,绘制之前,讲一下JPanel的绘制,在绘制图形时,是从上往下,从左到右绘制的:
- CELL_SIZE,是一个常量,用来描述一个单元格的宽度,这个在游戏当中是26,不用纠结这个数字,只是为了和游戏区域相符(为了好看),直接在Tetris类当中创建这个常量即可。
- 之前说过,游戏主区域是一个20行10列的二维数组,所以这里用双层for循环来绘制每个小方块,从而形成游戏主区域。
- 在绘制时需要判断小方格也就是wall[i][j]是否有小方块,这是因为当方块不能再下落时,需要嵌入到墙中,也就是绘制一张小方块的图片,并把四格方块的坐标赋给wall。
/*小方格宽度*/
private static final int CELL_SIZE=26;
public void paintWall(Graphics a) {
//外层循环控制行数
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)//判断所在单元格是否无方块
{
a.drawRect(x, y, CELL_SIZE, CELL_SIZE);
}
else
{
a.drawImage(cell.getImage(),x,y,null);
}
}
}
}
写好了以后,运行看看效果。
会发现墙的位置和预期想的位置不一样,这就是之前有提过,从上至下,从左至右的绘制规则,并且,游戏的主区域在游戏背景当中并不是从左上角开始的,稍微有点偏移,现在,就把这一点点偏移量加到paint方法当中去。
在paint中添加以下代码,以下代码的作用就是平移坐标轴,横坐标和纵坐标的偏移量大概是15:
//平移坐标轴
g.translate(15, 15);
然后,我们在绘制其他游戏元素,想要绘制什么东西,就封装好一个绘制的方法,然后在paint方法中调用即可。
二. 绘制正在下落的方块
首先,取得随机生成的四格方块,赋给Cell数组,遍历Cell数组,取得每个小方格的行号、列号乘以宽度,将每个小方格作为图片画到游戏主区域当中。
1.说一下为什么要乘以宽度,之前说过JPanel的绘制规则,并且创建的游戏主区域,也就是墙Wall是一个由26*26的正方形组成的20*10*正方形的大矩形,绘制下落的四格方块,就是绘制4个小方格到主区域当中,并且小方格的宽度就是正方形的宽度,所以,需要根据小方格的坐标来乘以宽度最后绘制出四格方块的形状。
/*绘制正在下落的四格方块
* 取出数组的元素
* 绘制元素的图片
* 横坐标x
* 纵坐标y
*/
public void paintCurrentOne(Graphics g){
Cell[] cells = currentOne.cells;
for(Cell c:cells)
{
int x = c.getCol()*CELL_SIZE;
int y = c.getRow()*CELL_SIZE;
g.drawImage(c.getImage(),x,y,null);
}
}
三. 绘制下一个将要下落的四格方块
原理和绘制正在下落的方块一样,主要是所在游戏场景位置不同,需要加上偏移量。
public void paintNextOne(Graphics g) {
//获取nextOne对象的四个元素
Cell[] cells = nextOne.cells;
for(Cell c:cells) {
//获取每一个元素的行号和列号
int row = c.getRow();
int col = c.getCol();
//横坐标
int x = col*CELL_SIZE+260;
//纵坐标
int y = row*CELL_SIZE+26;
g.drawImage(c.getImage(),x,y,null);
}
}
四. 绘制游戏得分
首先,需要创建以下常量用于存储游戏分数。
- scores_pool,游戏分数池,根据一次消除的行数数量不同,得分也不同,消一行得1分,消两行得2分,消三行得5分,最多消四行,得10分。
- totalScore,当前获得的游戏分数。
- totalLine,当前已消除的行数。
/*统计分数*/
int[] scores_pool = {0,1,2,5,10};
private int totalScore = 0;
private int totalLine = 0;
用paintScore方法来绘制游戏得分:
- g.setFont是设置字符串的格式,字体、大小等。
- g.drawString之前说过是用来绘制字符串的。
public void paintScore(Graphics g) {
g.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 30));
g.drawString("SCORES:"+totalScore, 285, 160);
g.drawString("LINES:"+totalLine, 285, 215);
}
运行效果
五. 绘制游戏状态
接下来,来绘制游戏状态,游戏分为三个状态,游戏中,暂停,游戏结束,用常量来充当游戏状态,并定义一个变量来存储当前游戏状态。
/*定义三个常量:充当游戏的状态*/
public static final int PLAYING = 0;
public static final int PAUSE = 1;
public static final int GAMEOVER = 2;
/*定义一个属性,存储游戏的当前状态*/
private int game_state;
1. paintState,用来绘制游戏的当前状态,在绘制之前,创建一个字符串数组,用来显示游戏状态。即当游戏运行时,显示按P暂停,游戏暂停时,显示按C继续,游戏结束时,显示按S重新开始。
String[] show_state = {"P[pause]","C[continue]","S[replay]"};
public void paintState(Graphics g) {
if(game_state == GAMEOVER) {
g.drawImage(gameover, 0, 0, null);
g.drawString(show_state[GAMEOVER], 285, 265);
}
else if (game_state == PLAYING) {
g.drawString(show_state[PLAYING], 285, 265);
}
else if (game_state == PAUSE) {
g.drawString(show_state[PAUSE], 285, 265);
}
}
查看后续教程,Java俄罗斯方块 ---(二)游戏操作与逻辑篇