本小程序为Java语言,为Java面向对象知识总结
由于Java语言为面向对象编程语言,在制作这个小游戏之前,考虑对游戏内容(对象)的划分
ps:在此处,采用的是在游戏区域,只能放下10*20个小方格的区域
首先划分出Cell类--每个方块都是由四个小方块(cell)组成,每个小方块都具有的属性,方法,然后提供相应的get/set方法,构造方法(一般最少提供俩个无参/全参),与及toString方法;
import java.awt.image.BufferedImage;
public class Cell {
private int row; //定义行号
private int col; //定义列号
private BufferedImage image; //定义方块的背景图片
public void left() { //向左移
col--;
}
public void right() { //向右移
col++;
}
public void drop() { //下落
row++;
}
public Cell(int row, int col, BufferedImage image) {
this.row = row;
this.col = col;
this.image = image;
}
public Cell() {}
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() {
return "Cell [row=" + row + ", col=" + col + ", image=" + image + "]";
}
}
public class Tetromino {
protected Cell[] cells = new Cell[4];
//方块左移
public void moveLeft() {
for (Cell c : cells) {
c.left();
}
}
//方块右移
public void moveRight() {
}
//方块下落
public void moveDrop() {
}
}
//例如图形T
public class T extends Tetromino{ 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); }}
依此,我们能得出其他方块的坐标与及形状。
在主类中,我们用来加载静态资源和获取第3步所需的图片(利用ImageIO流获取文件资源),由于涉及到IO流,就涉及到异常
//定义各种静态图片变量,
//public static BufferedImage T....
static {
try {
/*
* getResource(String Url)
* Url:加载图片的路径
* 相对路径是Src下
*/
/*
* Class.getResources(String path) path如果是以 / 开头,就从classpath中去找
*(classpath可以认为是eclipse的bin目录或者是target的classes目录),如果
* 不以/开头,就以当前类的位置开始找,假如实在上一级目录,那么就是 ../application/xml.
*/
T = ImageIO.read(Tetris.class.getResource("T.png"));
} catch (Exception e) {
e.printStackTrace();
}
}
同时我们在主类中,添加主方法--小游戏的入口(在这里我们采用Swing框架的一些基础知识,来实现窗口,同时让主类继承JPanel类,实现面板中的背景图片形成)
//游戏开始程序入口
public static void main(String[] args) {
//1.创建一个窗口
JFrame frame = new JFrame("小游戏");
//2.设置窗口尺寸
frame.setSize(541,590);
//3.设置窗口居中
frame.setLocationRelativeTo(null);
//4.设置窗口关闭
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//5.创建游戏界面,即面板
Tetris panel = new Tetris();
//6.将面板嵌入窗口
frame.add(panel);
//7.设置为可见
frame.setVisible(true);//一般将这一属性在最后设置,避免导致运行后,背景加载不出来,出现窗口的空白
}
由于每个方块的出现都是随机性的,我们在这里利用随机数来决定此时该下落的方块和将要下落的方块形状,在Tetromino类中封装一个判断方块形状的随机方法
//随机生成一个四个方块,以随机数的形来代表该出现的是哪一个方块
public static Tetromino randomOne() {
Tetromino t = null;
int num = (int)(Math.random()*7);
switch(num) {
case 0 : t = new T();break;
//其他数....
}
return t;
}
在主类中定义将要下落方块和正在下落的方块
//正在落下的四方块
private Tetromino currentOne = Tetromino.randomOne();
//将要落下的四方块
private Tetromino nextOne = Tetromino.randomOne();
//20*10墙
private Cell[][] wall= new Cell[20][10];
此时我们可以利用重写Jpanel类中的paint方法来绘制背景图片与及背景墙,方块等等
//拿到画笔Graphics,重写paint()
public void paint(Graphics g) {
g.drawImage(BGImg, 0, 0, null);
g.translate(15, 15);//由于我们绘制背景图的时候,该背景墙格的起始位置并非(0,0)点,因而我们再次需要进行墙的位置平移
paintWall(g);//画出背景墙
paintCurrentOne(g);//画出正在下落的方块
paintNextOne(g);//画出下一个要下落的方块
}
然后实现paint中的方法
//画出背景墙
public void paintWall(Graphics g) {
for(int i = 0;i<20;i++) {
for (int j = 0; j < 10; j++) {
int x = j*cellSize;
int y = i*cellSize;
Cell cell = wall[i][j];
if(cell==null) {
g.drawRect(x,y,cellSize,cellSize);
}else {
g.drawImage(cell.getImage(), x, y, null);
}
}
}
}
//画出正在下落的四方块
public void paintCurrentOne(Graphics g) {
Cell[] cells = currentOne.cells;
for(Cell c:cells) {
int x = c.getCol()*cellSize;
int y = c.getRow()*cellSize;
g.drawImage(c.getImage(), x, y, null);
}
}
//画出将要下落的四方块
public void paintNextOne(Graphics g) {
Cell[] cells = nextOne.cells;
for(Cell c:cells) {
int x = c.getCol()*cellSize+260;//由于在这里,将要下落的方格并不在所画的背景墙的范围,因此我们可以进行平移,当然,也可以采用上述translate方法在paint方法中进行平移
int y = c.getRow()*cellSize+26;
g.drawImage(c.getImage(), x, y, null);
}
}
在图片等资源加载完成后,进行游戏开始
public void start() {
while(true) {
try {
Thread.sleep(1000);//在此处采用线程的sleep方法,来控制方块的自动下降速度
} catch (InterruptedException e) {
e.printStackTrace();
}
if(canDrop()) {
currentOne.moveDrop();
}else{
landToWall();//当方块不能再下降的时候,将方块中的每个cell对象嵌入墙中
currentOne = nextOne;
nextOne = Tetromino.randomOne();
}
repaint();//进行重新绘制,达到页面的刷新效果
}
}
//判断方块能否下落
public boolean canDrop() {
Cell[] cells = currentOne.cells;
for(Cell c:cells) {
int row = c.getRow();
int col = c.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 c:cells) {
int row = c.getRow();
int col = c.getCol();
wall[row][col] = c;
}
//当墙的最上一行有方块填充的时候,游戏结束
for(int i = 0;i<10;i++) {
if(wall[0][i]!=null) {
System.exit(0);
}
}
}
监听键盘上下左右键,来进行方块的移动(此处我们利用方法内部类和Keylistenner类)
KeyListener l = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
switch(e.getKeyCode()) {
case KeyEvent.VK_DOWN:
dropAction();
break;
case KeyEvent.VK_LEFT:
leftAction();
break;
case KeyEvent.VK_RIGHT:
rightAction();
break;
case KeyEvent.VK_UP:
changeAction();
break;
}
repaint();
}
};
this.addKeyListener(l);//按键监听
this.requestFocus();//获取焦点
//方块下落函数
public void dropAction() {
//判断是否具备下落的条件,当不能再下落后,将方块中的对象值赋予给对应墙所在位置的对象
}
//方块左/右移函数
public void leftAction() {
//先行移动,再判断此时的方块是否出界或者重合(所在的位置所对应墙的位置的对象值是否不为空),如果重合或者越界,就往反方向移回来
}
接下来就是旋转的问题:对于每个方块都有其对应的几个旋转后的形状结果,而且其旋转也是一种周期性的,拿“T”来说吧,他有以下几种方式
在这里呢,既然他们的旋转之后的结果图形数量是一定的(最多不超过四种),那么我们可以在定义该类型方块类的时候,就一起将其旋转后的图形结果(在这里,最好是采用相对值,及相对某一个小方格的位置)放在一个数组之中,然后在每次调用旋转方法的时候,就直接从该数组中取出相应的值
对于旋转,基本思路与左右移动一致,我们采取一般是采取顺时针旋转(向右旋转,取数组下一个值),我们在每次旋转后对旋转结果进行判断一下,判断是否存在小方格出界或者其对应墙位置的的对象值不为null,如果为null,咱们就将其逆时针旋转一次(向左旋转,取此时对应的上一个值)
clickNum++;//定义的一个变量值,通过以此来,,旋转顺序
int col = currentOne.cells[0].getCol();
int row = currentOne.cells[0].getRow();
int index = clickNum % currentOne.states.length;
currentOne.cells[1].setCol(currentOne.states[index].getCol1() + col);
currentOne.cells[1].setRow(currentOne.states[index].getRow1() + row);
currentOne.cells[2].setCol(currentOne.states[index].getCol2() + col);
currentOne.cells[2].setRow(currentOne.states[index].getRow2() + row);
currentOne.cells[3].setCol(currentOne.states[index].getCol3() + col);
currentOne.cells[3].setRow(currentOne.states[index].getRow3() + row);
if (outofBounds() || coincide()) {
rotateLeftAction();
}
对于这个问题呢,我们就得考虑以下问题了:
1.如何判断该行已经满行,
2.消除行的时候,应该是在什么时候调用这个方法,在对象值嵌入墙内之前/后?
3.消除行的时候是应该从哪开始消除,上/下?。。。。
显然,我们应该在嵌入墙之后再判断该方格每行对应的墙的行的每个元素,如有存在值为null的,即不满足消行条件,否则就进行消行。
在消除行的时候,我采用的思路是,首先比较该方格的每个小方块对应的行值,取出其最大值,然后,咱们就从该行开始进行判断消除的满足条件,满足就开始消除此行,然后将上一行墙的值赋给当前行,再次进行判断,当发现该行不满足消除的条件的时候,咱们就判断上一行,依次循环,直到将最上一行赋值给第二行,再将最上一行的值清空。
public void removeLine() {
Cell[] cells = currentOne.cells;
int row = cells[0].getRow();
for (int i = 0; i < cells.length; i++) {
if(cells[i].getRow() >= row) {
row = cells[i].getRow();
}
}
for (int i = row; i >0 ; i--) {
while(true) {
if(isFullLine(wall[i])) {
wall[i]=new Cell[10];
for(int j = i;j>0;j--) {
wall[j] = wall[j-1];
}
wall[0]=new Cell[10];
}else {
break;
}
}
}
}
得分消行统计这些算法很简单,怎么实现我不管。
我们将这些值以字符串的形式直接画到右边的三个白框中,在此处用到了画笔的drawString(String,x,y),(ps:在这里画字符串的时候,它是以字符串的左下角为起始点)
定义的 一些静态的常量(状态量),然后我们事件监听的函数中添加对一些按键的监听,通过对该静态变量的判断,分别来控制其暂停等功能方法,
if (code == KeyEvent.VK_P) { //P键暂停
if (game_state == PLAYING) {
game_state = PAUSE;
}
}
if (code == KeyEvent.VK_C) { //C键继续
if (game_state == PAUSE) {
game_state = PLAYING;
}
}
if (code == KeyEvent.VK_ENTER) { //Enter键重新开始
game_state = PLAYING;
wall = new Cell[20][10];
currentOne = Tetromino.randomOne();
nextOne = Tetromino.randomOne();
totalScore = 0;
totalLine = 0;
}
写完这个,整个人都不开心了
public void moveRight() {
}