一点一点写出来的程序,想跟大家分享一下自己的心得,可能有错误,还请多多包涵~
Cell类:
public class Cell
定义了本游戏最基本的元素:小方块(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;
}
提供get/set方法:
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 void left() {
col--;//向左移动,列数减1
}
public void right() {
col++;//向右移动,列数加1
}
public void drop() {
row++;//向下移动,行数+1
}
Tetromino类,提供方块的各种行为方法:
public class Tetromino{
我们将一个四个方块视为一个数组:
protected Cell[] cells = new Cell[4];
将一个四个方块的四种状态也定义为数组:
protected State[] states;
添加一个作为旋转计数器的量:
private int count = 100000;//数值多少都可以
定义cell的各种行为:下落;左移;右移;旋转:
public void moveLeft() {//向左移动
for(int i=0;i
以及旋转的方法
public void rotateRight() {//向右旋转
//旋转有一次,计算器增长1
count++;//100001
State s = states[count%states.length];
Cell c = cells[0];
int row = c.getRow();
int 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--;//100001
State s = states[count%states.length];
Cell c = cells[0];
int row = c.getRow();
int 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);
}
以及随机生成七种方块中的一种的方法randomOne()
public static Tetromino randomOne() {
//随机生成方块,七种方块形状分别为O,T,I,J,L,S,Z
Tetromino t = null;
int num = (int)(Math.random()*7);
switch(num) {
case 0:t = new O();break;
case 1:t = new T();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;
}
然后定义内部类state:
public class State{
此类用来描述方块旋转的四种状态,首先定义八个整型变量,用来描述四个方块的位置.0,1,2,3分别代表四个方块,我们旋转的时候以方块0为轴,其余三个方块向右旋转:
int row0,col0,row1,col1,row2,col2,row3,col3;
然后提供无参有参构造器:
public State() {}
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;
}
get/set方法:
public int getRow0() {
return row0;
}
public void setRow0(int row0) {
this.row0 = row0;
}
public int getCol0() {
return col0;
}
public void setCol0(int col0) {
this.col0 = col0;
}
public int getRow1() {
return row1;
}
public void setRow1(int row1) {
this.row1 = row1;
}
public int getCol1() {
return col1;
}
public void setCol1(int col1) {
this.col1 = col1;
}
public int getRow2() {
return row2;
}
public void setRow2(int row2) {
this.row2 = row2;
}
public int getCol2() {
return col2;
}
public void setCol2(int col2) {
this.col2 = col2;
}
public int getRow3() {
return row3;
}
public void setRow3(int row3) {
this.row3 = row3;
}
public int getCol3() {
return col3;
}
public void setCol3(int col3) {
this.col3 = col3;
}
之后我们来定义七种方块(O,T,I,J,L,S,Z):
(注意七种方块均应该继承Tetromino类)
这里以T型为例详细讲解:
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);//状态0
states[1] = new State(0,0,-1,0,1,0,0,-1);//状态1
states[2] = new State(0,0,0,1,0,-1,-1,0);//状态2
states[3] = new State(0,0,1,0,-1,0,0,1);//状态三
}
}
O型方块:
public class O extends Tetromino {
public O() {
cells[0]=new Cell(0,4,Tetris.O);
cells[1]=new Cell(0,5,Tetris.O);
cells[2]=new Cell(1,4,Tetris.O);
cells[3]=new Cell(1,5,Tetris.O);
states = new State[] { new State(0, 0, 0, 1, 1, 0, 1, 1)};
}
}
I型方块:
public class I extends Tetromino {
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);
states = new State[] {
new State(0, 0, 0, -1, 0, 1, 0, 2),
new State(0, 0, -1, 0, 1, 0, 2, 0)};
}
}
J型方块:
public class J extends Tetromino{
public J() {
cells[0]=new Cell(0,4,Tetris.J);
cells[1]=new Cell(0,3,Tetris.J);
cells[2]=new Cell(0,5,Tetris.J);
cells[3]=new Cell(1,5,Tetris.J);
states = new State[] {
new State(0, 0, 0, 1, 0, -1, -1, -1),
new State(0, 0, 1, 0, -1, 0, -1, 1),
new State(0, 0, 0, -1, 0, 1, 1, 1),
new State(0, 0, -1, 0, 1, 0, 1, -1)};
}
}
L型方块
public class L extends Tetromino {
public L() {
cells[0]=new Cell(0,4,Tetris.L);
cells[1]=new Cell(0,3,Tetris.L);
cells[2]=new Cell(0,5,Tetris.L);
cells[3]=new Cell(1,3,Tetris.L);
states = new State[] {
new State(0, 0, 0, 1, 0, -1, -1, 1),
new State(0, 0, 1, 0, -1, 0, 1, 1),
new State(0, 0, 0, -1, 0, 1, 1, -1),
new State(0, 0, -1, 0, 1, 0, -1, -1)};
}
}
S型方块:
public class S extends Tetromino{
public S() {
cells[0]=new Cell(0,4,Tetris.S);
cells[1]=new Cell(0,5,Tetris.S);
cells[2]=new Cell(1,3,Tetris.S);
cells[3]=new Cell(1,4,Tetris.S);
states = new State[] {
new State(0, 0, 0, 1, 1, -1, 1, 0),
new State(0, 0, -1, 0, 1, 1, 0, 1)};
}
}
Z型方块:
public class Z extends Tetromino {
/**
* 提供构造器,进行初始化
* Z型的四格方块的位置
* */
public Z() {
cells[0]=new Cell(1,4,Tetris.Z);
cells[1]=new Cell(0,3,Tetris.Z);
cells[2]=new Cell(0,4,Tetris.Z);
cells[3]=new Cell(1,5,Tetris.Z);
states = new State[] {
new State(0, 0, -1, -1, -1, 0, 0, 1),
new State(0, 0, -1, 1, 0, 1, 1, 0)};
}
}
主类Tetris类:游戏的核心,使用JPanel绘制游戏界面:
public class Tetris extends JPanel{
首先定义正在下落和即将下落的四格方块:
private Tetromino currentOne = Tetromino.randomOne();//正在下落
private Tetromino nextOne = Tetromino.randomOne();//下一个下落
定义一个叫做墙的二维数组作为游戏界面:
private Cell[][] wall = new Cell[20][10];//20行10列
定义分数池,作为消除0,1,2,3,4列的得分:
int[] scores_pool = { 0, 1, 2, 5, 10 };
这里用来统计游戏分数和已经消除的行数:
private int totalScore = 0;//总分
private int totalLine = 0;//总行数
定义游戏的三种状态常量:正在游戏0,暂停1,游戏结束2:
public static final int PLAYING = 0;
public static final int PAUSE = 1;
public static final int GAMEOVER = 2;
游戏状态:
private int game_state;
这个数组用来显示游戏状态文字:
String[] showState = { "P[pause]", "C[continue]", "Enter[replay]" };
定义常量:方块的边长,已知是26:
private static final int CELL_SIZE = 26;//常量应使用private static final修饰.
游戏界面的各种图片,此时我们应该将图片放入此项目的包内:
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 game_over;//游戏结束
如何读取资源呢?这里应该使用静态代码块进行读取,为了防止各种意外,我们将其放入try....catch中,这时会读取包内的同名图片:
static {
try {
//getResource(String url) url:加载图片的路径 相对位置是同包下
T = ImageIO.read(Tetris.class.getResource("T.png"));
O = ImageIO.read(Tetris.class.getResource("O.png"));
I = ImageIO.read(Tetris.class.getResource("I.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();
}
}
接下来绘制游戏的各种图片,需要使用JPanel类中的paint()方法:
public void paint(Graphics g) {
// 绘制背景,在区域1
//g:画笔 g.drawImage(image,x,y,null) image:绘制的图片 x:开始绘制的横坐标 y:开始绘制的纵坐标
g.drawImage(background, 0, 0, null);
// 平移坐标轴
g.translate(15, 15);
// 绘制墙
paintWall(g);
// 绘制正在下落的四格方块,在区域5
paintCurrentOne(g);
// 绘制下一个将要下落的四格方块,在区域2
paintNextOne(g);
paintScore(g);//绘制游戏分数和列数,分数在区域3,列数在区域4
paintState(g);//绘制游戏状态,在区域6
}
private void paintState(Graphics g) {//在右侧绘制游戏状态
if (game_state == GAMEOVER) {//游戏结束
g.drawImage(game_over, 0, 0, null);
g.drawString(showState[GAMEOVER], 285, 265);
}
if (game_state == PLAYING) {//正在游戏
g.drawString(showState[PLAYING], 285, 265);
}
if (game_state == PAUSE) {//暂停游戏
g.drawString(showState[PAUSE], 285, 265);
}
}
public void paintScore(Graphics g) {//在右侧位置绘制游戏分数
g.setFont(new Font(Font.SANS_SERIF, Font.ITALIC, 26));
g.drawString("SCORES:" + totalScore, 285, 165);
g.drawString("LINES:" + totalLine, 285, 215);
}
/**
* 绘制下一个将要下落的四格方块 绘制到面板的右上角的相应区域
*/
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);
}
}
/**
* 绘制正在下落的四格方块 取出数组的元素 绘制元素的图片, 横坐标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);
}
}
/**
* 墙是20行,10列的表格 是一个二维数组, 应该使用双层循环 绘制正方形。
*/
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);
}
}
}
}
之后开始设置游戏的各种状态:
布尔型方法,游戏是否结束:
public boolean isGameOver() {
Cell[] cells = nextOne.cells;
for (Cell c : cells) {
int row = c.getRow();
int col = c.getCol();
if (wall[row][col] != null) {//若方块已经达到第20行,则游戏结束
return true;
}
}
return false;
}
下落之后就要判断一行是否填满以便进行消除,所以我们定义布尔型方法,带参数row:
public boolean isFullLine(int row) {
把一行定义为一个数组进行遍历:
Cell[] line = wall[row];
for (Cell c : line) {
if (c == null) {//遍历到为空的方块即返回false,表明这一行没有满.
return false;
}
}
return true;
}
关键的方法:消除
public void destroyLine(){
若其中一行满了则需进行消除,首先定义变量来统计消除的行数:
int lines = 0;
然后进入方法:
Cell[] cells = currentOne.cells;
for (Cell c : cells) {
int row = c.getRow();//无需判断列数,所以不需要col
while (row < 20) {
if (isFullLine(row)) {//判断是否消除
lines++;//消除的行数+1
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 canDrop() {
该方法用来判断currentOne能否继续下落,只要这个方块的一个元素的下一行存在方块(不是null)或者已经到达底部则停止下落:
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;
}
不能下落之后就应该着陆了,然后应该把它镶嵌进wall中,即存储到wall[][]中:
public void landToWall() {
Cell[] cells = currentOne.cells;
for (Cell c : cells) {
// 获取最终的行号和列号
int row = c.getRow();
int col = c.getCol();
wall[row][col] = c;
}
}
为了防止游戏错误,我们应该设计两个方法来进行判定:
public boolean outOfBounds() {//越界异常
Cell[] cells = currentOne.cells;
for (Cell c : cells) {
int col = c.getCol();
int row = c.getRow();
if (col < 0 || col > 9 || row > 19 || row < 0) {//不能越过wall[][]
return true;
}
}
return false;
}
public boolean coincide() {//两个方块重合
Cell[] cells = currentOne.cells;
for (Cell c : cells) {
int row = c.getRow();
int col = c.getCol();
if (wall[row][col] != null) {
return true;
}
}
return false;
}
然后可以进行游戏的五种操作:左移,右移,缓慢下落,直接到底和旋转:
左移:
protected void moveLeftAction() {
currentOne.moveLeft();
if (outOfBounds() || coincide()) {//如果左移出了边界,执行右移的方法防止游戏错误
currentOne.moveRight();
}
}
右移:
protected void moveRightAction() {
currentOne.moveRight();
if (outOfBounds() || coincide()) {//如果右移出了边界,执行左移的方法防止错误.
currentOne.moveLeft();
}
}
缓慢下落:
public void softDropAction() {
if (canDrop()) {
currentOne.softDrop();
} else {
landToWall();
destroyLine();
currentOne = nextOne;//把这一个方块"变成"下一个方块
nextOne = Tetromino.randomOne();//再随机生成一个"下一个方块"
}
}
直接到底:
public void handDropAction() {
for (;;) {
if (canDrop()) {
currentOne.softDrop();
} else {
break;
}
}
landToWall();
destroyLine();
if (!isGameOver()) {
currentOne = nextOne;
nextOne = Tetromino.randomOne();
} else {
game_state = GAMEOVER;
}
}
旋转:
public void rotateRightAction() {
currentOne.rotateRight();
if (outOfBounds() || coincide()) {//转过头了怎么办?这就是rotateLeft()方法的用处了
currentOne.rotateLeft();
}
}
接下来把以上方法都编入start()
public void start() {//封装了游戏逻辑
将游戏状态置为PLAYING:
game_state = PLAYING;
游戏应该使用键盘操作,所以我们要开启键盘监听事件:
KeyListener l = new KeyAdapter() {
按键按下时即应该进行响应,注意此处keyPress()的k应该是小写,我就是因为这个导致很久没有运行成功:
public void keyPressed(KeyEvent e) {
定义code变量:
int code = e.getKeyCode();
按P(pause)键暂停游戏,前提是正在进行游戏:
if (code == KeyEvent.VK_P) {//VK_P即表示键盘P键
if (game_state == PLAYING) {//状态为PLAYING才能暂停
game_state = PAUSE;
}
}
按C(continue)键继续游戏:
if (code == KeyEvent.VK_C) {
if (game_state == PAUSE) {
game_state = PLAYING;
}
}
按回车键开始游戏:
if (code == KeyEvent.VK_ENTER) {
game_state = PLAYING;
wall = new Cell[20][10];//画一个新的"墙"
currentOne = Tetromino.randomOne();
nextOne = Tetromino.randomOne();
totalScore = 0;//分数置为0
totalLine = 0;//列数置为0
}
上下左右空格键来操作方块,这里利用switch循环:
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://按空格直接到底
handDropAction();
break;
}
repaint();//每操作一次都要重新绘制方块
}
};//内部类
把监听添加进面板,并把面板设置为焦点:
this.addKeyListener(l);
this.requestFocus();
由于CPU的速度极快,导致我们不能看清方块的下落,所以我们要设置延时,让CPU"睡眠"一段时间后再进行下一次下落:
while (true) {
/**
* 当程序运行到此,会进入睡眠状态, 睡眠时间为800毫秒,单位为毫秒 800毫秒后,会自动执行后续代码
*/
try {
Thread.sleep(800);
} catch (InterruptedException e) {
e.printStackTrace();
}
补充剩余部分:
if (game_state == PLAYING) {
if (canDrop()) {
currentOne.softDrop();
} else {
landToWall();
destroyLine();
// 将下一个下落的四格方块赋值给正在下落的变量
if (!isGameOver()) {
currentOne = nextOne;
nextOne = Tetromino.randomOne();
} else {
game_state = GAMEOVER;
}
}
repaint();
/*
* 下落之后,要重新进行绘制,才会看到下落后的 位置 repaint方法 也是JPanel类中提供的 此方法中调用了paint方法
*/
}
}
}
一切准备就绪,可以编写主方法来运行游戏了:
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, 580);
// 4:设置窗口居中
frame.setLocationRelativeTo(null);
// 5:设置窗口关闭,即程序终止
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 游戏的主要逻辑封装在start方法中
panel.start();
}
}
至此一个简单的俄罗斯方块小游戏就编写完成了,点击运行,即可开始游戏!
总结:一个小程序打下来,感觉收获颇多,对面向对象编程更加熟悉了,打算多打几遍,争取只看简单的提示就可以完成这个程序.