一点一点写出来的程序,想跟大家分享一下自己的心得,可能有错误,还请多多包涵~
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<cells.length;i++)
{//for循环遍历整个"方块组"的四格方块 Cell cell = cells[i];//四格方块都要移动 cell.left(); } } public
void moveRight() {//向右移动 for(Cell c:cells) {//此处使用增强for循环也可以 c.right(); } }
public void softDrop() {//下落 for(Cell c:cells) { c.drop(); } }
以及旋转的方法
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(); } }
至此一个简单的俄罗斯方块小游戏就编写完成了,点击运行,即可开始游戏!
总结:一个小程序打下来,感觉收获颇多,对面向对象编程更加熟悉了,打算多打几遍,争取只看简单的提示就可以完成这个程序.
热门工具 换一换