规则
俄罗斯方块相信大家小时候都玩过,今天我们用Java来实现简易版俄罗斯方块。我们先了解其基本规则:
- 方块会从上方缓慢下落,玩家可以通过键盘上的上下左右键来控制方块。
- 方块移到区域最下方或是着地到其他方块上无法移动时,就会固定在该处,而新的方块出现在区域上方开始落下。
- 当区域中某一列横向格子全部由方块填满,则该列会消失并成为玩家的得分。同时删除的列数越多,得分指数上升。
- 当固定的方块堆到区域最上方而无法消除层数时或者大于游戏区域时,则游戏结束。
tip: 这里的规则图形是由4个小型正方形组成的,英文称为Tetromino,中文通称为方块共有7种,分别以S、Z、L、J、I、O、T这7个字母的形状来命名。
趣味小知识: 你知道为什么俄罗斯方块只有这几种形状吗?
答案:如果这四个小正方形拼起来只有一层,那么只有一种形状,我们可以把这种形状称为 “长条”,如果这四个小正方形拼起来形成两层,那么可以有两种情况:“上一下三"和"上二下二”,对于"上二下二”的情况,显然也有三种拼法,我们从左到右,把这三种形状分别称为 “左二二形”,“四方形"和"右二二形”.
准备工作
编译软件:IntelliJ IDEA JDK版本:8.0 使用素材:
小方块和背景图片:链接 源码:链接
编写小方块类
由于基本方块是由4个小方块组成,所以我们可以把小方块单独抽象为Cell类.
小方块属性:小方块的行,列坐标以及每个单元格的图片 小方块的方法:左移一格,右移一格,下移一格。
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) {
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;
}
public void left() {
col--;
}
public void right() {
col++;
}
public void soft() {
row++;
}
}
编写四方格父类
四方格都是由四格小方块组成,都可以进行左移,右移,下移以及它特有的变形(变形相对复杂,暂时不写)
public class Tetromino {
protected Cell[] cells = new Cell[4];
public void moveLeft() {
for (Cell cell : cells) {
cell.left();
}
}
public void moveRight() {
for (Cell cell : cells) {
cell.right();
}
}
public void sftDrop() {
for (Cell cell : cells) {
cell.drop();
}
}
}
创建7种不同的形状
7 个不同的形状分别使用 I、 T、 L、 J、 S、 Z、 O 表示,并且继承四方个父类用于初始化小方块的位置达到形成基本形状的作用。
public class I extends Tetromino{
}
public class J extends Tetromino{
}
public class L extends Tetromino{
}
public class O extends Tetromino{
}
public class S extends Tetromino{
}
public class T extends Tetromino{
}
public class Z extends Tetromino{
}
编写俄罗斯方块主类
编写俄罗斯方块主类并并且继承JPanel框架,然后静态载入图片:
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
public class Tetris extends JPanel {
public static BufferedImage I;
public static BufferedImage J;
public static BufferedImage L;
public static BufferedImage O;
public static BufferedImage S;
public static BufferedImage T;
public static BufferedImage Z;
static {
try {
I = ImageIO.read(new File("images/I.png"));
J = ImageIO.read(new File("images/J.png"));
L = ImageIO.read(new File("images/L.png"));
O = ImageIO.read(new File("images/O.png"));
S = ImageIO.read(new File("images/S.png"));
T = ImageIO.read(new File("images/T.png"));
Z = ImageIO.read(new File("images/Z.png"));
} catch (IOException e) {
e.printStackTrace();
}
}
}
初始化7种形状
按照图片规则来放置小方块,方便变形
- 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);
}
}
- 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);
}
}
- 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);
}
}
- 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);
}
}
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);
}
}
- 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);
}
}
public class Z extends Tetromino{
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);
}
}
随机生成四方格
在四方格父类种编写随机生成7种形状的静态方法:
public static Tetromino randomOne() {
int num = (int)(Math.random() * 7);
Tetromino tetromino = null;
switch (num) {
case 0 :
tetromino = new I();
break;
case 1 :
tetromino = new J();
break;
case 2 :
tetromino = new L();
break;
case 3 :
tetromino = new O();
break;
case 4 :
tetromino = new S();
break;
case 5 :
tetromino = new T();
break;
case 6 :
tetromino = new Z();
break;
}
return tetromino;
}
在俄罗斯方块类 Tetris 中,抽象相应的成员:
currentOne 表示正在下落的方块 nextOnt 表示将要下落的方块 wall 表示游戏主区域
private Tetromino currentOne = Tetromino.randomOne();
private Tetromino nextOne = Tetromino.randomOne();
private Cell[][] wall = new Cell[18][9];
创建游戏场景
在俄罗斯方块主类中创建main方法,并且创建游戏窗口(810 * 940)
public static void main(String[] args) {
JFrame frame = new JFrame("俄罗斯方块");
frame.setVisible(true);
frame.setSize(810,940);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
绘制游戏
绘制游戏背景
- 在main方法中,先创建游戏界面并嵌入到窗口中
public static void main(String[] args) {
JFrame frame = new JFrame("俄罗斯方块");
Tetris panel = new Tetris();
frame.add(panel);
frame.setVisible(true);
frame.setSize(810,940);
frame.setLocationRelativeTo(null);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
- 重写paint方法
增加代码和运行图如下:
绘制游戏主区域
绘制游戏主区域(18行9列的二维数组),由于小方块的像素为48,所以在绘制主区域时要把每个单元格的宽度和高度设置为48像素,
- 声明单元格像素为48像素
- 绘制游戏主区域
效果图: 我们发现游戏主区域不在左上角,所以我们要添加偏移值,效果图:
绘制正在下落的四方格
首先获取随机生成四方格赋给 Cell 数组,然后遍历 Cell 数组,取得每个小方格的行号和列号乘以宽度值,将每个小方格作为图片画到游戏主区域中,然后再在 paint 方法中调用,
绘制下一个下落的四方格
原理同上,但是要设置偏移值,因为它应该出现在右边第一个方框中;
绘制游戏得分
创建存储分数的一系列变量:
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 用于绘制字符串
绘制游戏状态
游戏共有三种状态:游戏中,暂停游戏,游戏结束,用常量来标记;再用一个常量表面当前状态:
用一个数组来显示当前游戏状态:
在paint中编写paintState(g)方法显示当前游戏状态
编写游戏逻辑
判断方块是否出界
public boolean outOfBounds() {
Cell[] cells = currentOne.cells;
for (Cell cell : cells) {
int col = cell.getCol();
int row = cell.getRow();
if (col < 0 || col > COL - 1 || row < 0 || row > ROW - 1) {
return true;
}
}
return false;
}
- 我们用静态变量代表当前的行列
判断方块是否重合
public boolean coincide() {
Cell[] cells = currentOne.cells;
for (Cell cell : cells) {
int col = cell.getCol();
int row = cell.getRow();
if (wall[row][col] != null) {
return true;
}
}
return false;
}
按键一次左移一次和右移一次
public void moveLeftAction() {
currentOne.moveLeft();
if (outOfBounds() || coincide()) {
currentOne.moveRight();
}
}
public void moveRightAction() {
currentOne.moveRight();
if (outOfBounds() || coincide()) {
currentOne.moveLeft();
}
}
四方格变形
记录每个方块在不同形状时相对于序号为0的小方块的位置
首先,在四方格父类 Tetromino 中添加**旋转状态属性和计数器,**计数器的取值最好设置为能被 4 整除的数.用计数器对当前状态取余即可得到当前旋转状态。 在四方格父类中编写一个内部类旋转状态 State,属性:存储四方格各元素的相对位置
protected State[] states;
protected int count = 10000;
class State {
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) {
this.row0 = row0;
this.col0 = col0;
this.row1 = row1;
this.col1 = col1;
this.row2 = row2;
this.col2 = col2;
this.row3 = row3;
this.col3 = col3;
}
public int getRow0() {
return row0;
}
public void setRow0(int row0) {
this.row0 = row0;
}
public int getRow1() {
return row1;
}
public void setRow1(int row1) {
this.row1 = row1;
}
public int getRow2() {
return row2;
}
public void setRow2(int row2) {
this.row2 = row2;
}
public int getRow3() {
return row3;
}
public void setRow3(int row3) {
this.row3 = row3;
}
public int getCol0() {
return col0;
}
public void setCol0(int col0) {
this.col0 = col0;
}
public int getCol1() {
return col1;
}
public void setCol1(int col1) {
this.col1 = col1;
}
public int getCol2() {
return col2;
}
public void setCol2(int col2) {
this.col2 = col2;
}
public int getCol3() {
return col3;
}
public void setCol3(int col3) {
this.col3 = col3;
}
}
初始化7个形状的相对坐标
- I 共计有两种旋转状态,在 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[2];
states[0] = new State(0,0,0,-1,0,1,0,2);
states[1] = new State(0,0,-1,0,1,0,2,0);
}
}
- T 共计有四种旋转状态,在 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);
states = new State[4];
states[0] = new State(0,0,0,-1,0,1,1,0);
states[1] = new State(0,0,-1,0,1,0,0,-1);
states[2] = new State(0,0,0,1,0,-1,-1,0);
states[3] = new State(0,0,1,0,-1,0,0,1);
}
}
- L 共计有四种旋转状态,在 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[4];
states[0] = new State(0,0,0,-1,0,1,1,-1);
states[1] = new State(0,0,-1,0,1,0,-1,-1);
states[2] = new State(0,0,0,1,0,-1,-1,1);
states[3] = new State(0,0,1,0,-1,0,1,1);
}
}
- J 共计有四种旋转状态,在 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[4];
states[0] = new State(0,0,0,1,0,1,1,1);
states[1] = new State(0,0,-1,0,1,0,1,-1);
states[2] = new State(0,0,0,1,0,-1,-1,-1);
states[3] = new State(0,0,1,0,-1,0,-1,1);
}
}
- S 共计有两种旋转状态,在 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[2];
states[0] = new State(0,0,0,1,1,-1,1,0);
states[1] = new State(0,0,1,0,-1,-1,0,-1);
}
}
- Z 共计有两种旋转状态,在 Z 无参构造方法中初始化两种状态的相对坐标:
public class Z extends Tetromino{
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[2];
states[0] = new State(0,0,-1,-1,-1,0,0,1);
states[1] = new State(0,0,-1,1,0,1,1,0);
}
}
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[0];
}
}
顺时针旋转四方格
在四方格父类 Tetromino 类中创建顺时针旋转四方格方法 rotateRight
public void rotateRight() {
if (states.length == 0) {
return;
}
count++;
State s = states[count % states.length];
Cell cell = cells[0];
int row = cell.getRow();
int col = cell.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--;
State s = states[count % states.length];
Cell cell = cells[0];
int row = cell.getRow();
int col = cell.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 rotateRightAction() {
currentOne.rotateRight();
if (outOfBounds() || coincide()) {
currentOne.rotateLeft();
}
}
判断游戏是否结束
也就是判断下一个将要出现方块的位置是否有方块:
public boolean isGameOver() {
Cell[] cells = nextOne.cells;
for (Cell cell : cells) {
int row = cell.getRow();
int col = cell.getCol();
if (wall[row][col] != null) {
return true;
}
}
return false;
}
消行并记分
当四个方块嵌入墙中,使得行满时则消除该行并将消除行以上的方块下落到对应行数同时计分。
- isFullLine方法判断当前行是否满
public boolean isFullLine(int row) {
Cell[] cells = wall[row];
for (Cell cell : cells) {
if (cell == null) {
return false;
}
}
return true;
}
- 创建销行方法destroyLine:
public void destroyLine() {
int line = 0;
Cell[] cells = currentOne.cells;
for (Cell cell : cells) {
int row = cell.getRow();
if (isFullLine(row)) {
line++;
for (int i = row; i > 0; i--) {
System.arraycopy(wall[i - 1],0,wall[i],0,wall[0].length);
}
wall[0] = new Cell[9];
}
}
totalScore += scores_pool[line];
totalLine += line;
}
判断四方格能否下落
public boolean canDrop() {
Cell[] cells = currentOne.cells;
for (Cell cell : cells) {
int row = cell.getRow();
int col = cell.getCol();
if (row == wall.length - 1) {
return false;
} else if(wall[row + 1][col] != null) {
return false;
}
}
return true;
}
按键一次四方格下落一个
首先判断能否下落,如果能下落就下移,否则将四方格嵌入墙中,同时判断是否消行,并且还要判断游戏是否结束,如果没有结束,则继续生成四方格,
public void sortDropAction() {
if (canDrop()) {
currentOne.softDrop();
} else {
landToWall();
destroyLine();
if (isGameOver()) {
game_state = GAMEOVER;
} else {
currentOne = nextOne;
nextOne = Tetromino.randomOne();
}
}
}
private void landToWall() {
Cell[] cells = currentOne.cells;
for (Cell cell : cells) {
int row = cell.getRow();
int col = cell.getCol();
wall[row][col] = cell;
}
}
瞬间下落
public void handDropAction() {
while (canDrop()) {
currentOne.softDrop();
}
landToWall();
destroyLine();
if (isGameOver()) {
game_state = GAMEOVER;
} else {
currentOne = nextOne;
nextOne = Tetromino.randomOne();
}
}
调用游戏逻辑完成游戏操作
编写一个 start 方法,用于调用游戏操作逻辑并监听键盘和描述游戏主要逻辑。
键盘监听事件并且设置四方格自动下落
- 通过调用匿名内部类重写 keyPressed 方法实现按键的响应,
- 在游戏中时,四方格每隔 0.5 秒下落一次,直到不能下落,则将四方格嵌入墙中,并且判断消行和游戏是否结束,每次下落都需要重绘一次游戏画面
public void start() {
game_state = PLAYING;
KeyListener listener = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();
switch (code) {
case KeyEvent.VK_DOWN:
sortDropAction();
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;
case KeyEvent.VK_P:
if (game_state == PLAYING) {
game_state = PAUSE;
}
break;
case KeyEvent.VK_C:
if (game_state == PAUSE) {
game_state = PLAYING;
}
break;
case KeyEvent.VK_R:
game_state = PLAYING;
wall = new Cell[ROW][COL];
currentOne = Tetromino.randomOne();
nextOne = Tetromino.randomOne();
totalLine = 0;
totalScore = 0;
break;
}
}
};
this.addKeyListener(listener);
this.requestFocus();
while(true){
if(game_state == PLAYING){
try {
Thread.sleep(700);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(canDrop()){
currentOne.softDrop();
}else{
landToWall();
destroyLine();
if(isGameOver()){
game_state = GAMEOVER;
}else{
currentOne = nextOne;
nextOne = Tetromino.randomOne();
}
}
}
repaint();
}
}
游戏逻辑封装在方法中
|