前言:
写俄罗斯方块需要一些材料,以下为百度云链接,点击下载(若链接失效,请联系我)
正文:
第一步:
分析游戏界面(以下界面并不完成,只是一部分),通过游戏界面,抽象出几种类型
从图中我们可以看出有一个表格(就是界限,我们称之为墙,在此例子中墙的大小为20行10列)和元胞(就是一个方格)。
俄罗斯方块下落的图案有七种类型,还会显示下一个图案的类型。这七种类型类似于字母T、I、O、J、L、S、Z,一种类型对应一种颜色。(意思就是T类型的就用黄色的四个元胞表示,O类型用红色的四个元胞表示,以下图片在资源包中含有)
然后每个类型都可以向左移动,向右移动和向下移动。
第二步:
定义类型
- 元胞用Cell类表示,表示一个方格
共同特征:行号、列号,图片
共同行为:向左、向右、向下移动、提供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) { 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; } }
未完待续。。。。