一、游戏简介
俄罗斯方块,相信大家都很熟悉这个游戏了。记得以前还在qq游戏里火过一阵子。这次的练习就是用java来实现这个小游戏,这次是带图形界面的游戏了,不再是简略版。
先看一下游戏的运行图片。
我们首先要完成对这个游戏界面的绘制,下面的游戏信息类是完成右边信息面板的绘制,地图类则是左边游戏区的绘制。绘制的具体方法在注释里都有详细提到。主要就是重写了paintComponent 的方法
二、游戏信息类
这是右边的游戏信息类,最上方的是一张图片和中间的4*4 的方格与下面的文字,都是通过paintComponent()这个方法来画上去的。需要注意的是画线的位置和其他文字的位置。在第一次操作计算的的时候最好 拿一张纸,画一画可以更好的理解
具体的逻辑都注释里都写的很详细
package com.yc.Tetris;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.LayoutManager;
import java.awt.Toolkit;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class GameInfo extends JPanel {
private JLabel score ;
private JLabel bestScore;
private JLabel bestOwner;
//数组存储形状
private int[] shape = null;
//构造函数
public GameInfo() {
init();
}
//初始化界面
private void init() {
//绝对定位
this.setLayout(null);
JLabel label_1 = new JLabel("得分");
JLabel label_5 = new JLabel("最高分");
//JLabel label_6 = new JLabel("保持者");
//设置字体颜色
label_1.setForeground(Color.red);
label_5.setForeground(Color.red);
//设置字体
Font font = new Font("微软雅黑",Font.BOLD,24);
label_1.setFont(font);
label_5.setFont(font);
//设置位置
label_1.setBounds(0,340,80,30);
label_5.setBounds(0,390,80,30);
score = new JLabel("0");
score.setForeground(Color.RED);
score.setFont(font);
score.setBounds(85,340,165,30);
bestScore = new JLabel("0");
bestScore.setForeground(Color.RED);
bestScore.setFont(font);
bestScore.setBounds(85,390,165,30);
//游戏温馨提示
JLabel label_3 = new JLabel("适当游戏益脑, 沉迷游戏伤身。");
JLabel label_4 = new JLabel("合理安排时间, 享受健康生活。");
Font f = new Font("微软雅黑",Font.BOLD,18);
label_3.setForeground(Color.ORANGE);
label_4.setForeground(Color.ORANGE);
label_3.setFont(f);
label_4.setFont(f);
label_3.setBounds(0,440,260,45);
label_4.setBounds(0,485,260,45);
this.add(label_1);
this.add(score);
this.add(bestScore);
this.add(label_3);
this.add(label_4);
this.add(label_5);
}
@Override
protected void paintComponent(Graphics g) {//g就是画笔
// TODO Auto-generated method stub
super.paintComponent(g);
ImageIcon img = new ImageIcon(Toolkit.getDefaultToolkit().getImage(Test.class.getResource("/com/yc/Tetris/images/pic2.jpg")));
//开始画图 画的那张图 x y 大小 绘制对象的容器
g.drawImage(img.getImage(), 0, 10, 260, 85, null);
//画方块
if(shape != null) {
for(int i = 0; i < shape.length; i++) {
if(shape[i] == 1) {
//先设置颜色
g.setColor(Color.orange);
// 40-130 80-130 120-130 160-130; 40-170 80-170 120-170 160-170
g.fillRect(40 + (i % 4) * 40, 130 + (i / 4) * 40, 40, 40); //从那个点开始画,画多宽 多高
}
}
}
//图片画完 开始画线
//循环画线
for(int i = 1; i < 6; i++) {
//一个小格子长40
g.setColor(Color.white);
g.drawLine(40, 90 + i*40, 200, 90 + i * 40);
g.drawLine( i * 40, 130, i * 40, 290);
}
}
public void showNext(int[] shape) {
this.shape = shape;
//每次调用这个方法,意味着要重新显示方块,那么每次都要重新绘制一下
repaint();
}
public void setScore(int score) {
this.score.setText(score + "");
}
public void setBestScore(int bestScore) {
this.bestScore.setText(bestScore + "");
}
}
三、游戏地图类
地图也是一个二维数组,墙壁的值为2,方块的值为1,空的值为0
左边的游戏区,具体的游戏的逻辑方法都在这里
需要解决的主要问题是:
1.方块的随机生成
2.方块的下落
3.方块的形态转变,左移,右移,加速下降
4.碰撞检测
5.方块的消除以及积分的增加
1.方块的随机生成
一共又7类方块的形状,每个方块有不同的形态
我们用一个长度为16 的一维数组来存储一种形态。
一个形状的四种形态又是一个数组
7种类型又是一维数组
所以用一个三维数组来存储方块的形态
通过随机函数随机数组的下标来随机生成方块
2.方块的下落
用一个定时器来重绘地图,每次重绘方块有下落一个单位
3.方块的形态转变
通过键盘监听事件实现,形态转变通过改变数组的下标实现,左移右移通过方块在地图上的位置的改变来实现
4.碰撞检测
当方块碰到墙壁和地图上其他的方块时,都要进行碰撞检测,具体的逻辑看详细代码。
5.方块的消除以及积分的增加,定义一个计数器,通过循环,当当前位置是1 的时候自加,当count的值等于一层的值的时候,消除该层,该层上方的全部下移。每消除一次积分增加
package com.yc.Tetris;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Random;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.Timer;
public class Map extends JPanel{
//先定义右边游戏区域的大小
private int width = 420;
private int height = 660;
private int score;
//方块的状态 种类
private int blockType;
private int turnState;
private int newType = -1;
private int newState;
//随机数
Random r = new Random();
//gameinfo 对象
private GameInfo gameInfo;
//音乐对象
private MusicUtil music;
//定时器
private Timer timer;
private int x;
private int y;
//最高分的文件
private File highestScore;
private boolean isGameOver = false;
//定义区域
public static int[][] map = new int[22][14];
//形状
public static int[][][] shapes = {
// 一
{ {
0,1,0,0,
0,1,0,0,
0,1,0,0,
0,1,0,0
},{
1,1,1,1,
0,0,0,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
0,1,0,0,
0,1,0,0,
0,1,0,0
},{
1,1,1,1,
0,0,0,0,
0,0,0,0,
0,0,0,0
} },
// 田
{ {
0,1,1,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,1,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,1,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,1,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
} },
// 7
{{
0,1,1,0,
0,0,1,0,
0,0,1,0,
0,0,0,0
},{
0,0,1,0,
1,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
0,1,0,0,
0,1,1,0,
0,0,0,0
},{
0,1,1,1,
0,1,0,0,
0,0,0,0,
0,0,0,0
}},
//反7
{{
0,1,1,0,
0,1,0,0,
0,1,0,0,
0,0,0,0
},{
1,1,1,0,
0,0,1,0,
0,0,0,0,
0,0,0,0
},{
0,0,1,0,
0,0,1,0,
0,1,1,0,
0,0,0,0
},{
0,1,0,0,
0,1,1,1,
0,0,0,0,
0,0,0,0
}},
//立
{{
0,1,0,0,
1,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
0,1,1,0,
0,1,0,0,
0,0,0,0
},{
1,1,1,0,
0,1,0,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
1,1,0,0,
0,1,0,0,
0,0,0,0
}},
// z
{{
0,0,1,1,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
0,1,1,0,
0,0,1,0,
0,0,0,0
},{
0,0,1,1,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
0,1,1,0,
0,0,1,0,
0,0,0,0
}},
// 反z
{{
1,1,0,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,0,1,0,
0,1,1,0,
0,1,0,0,
0,0,0,0
},{
1,1,0,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,0,1,0,
0,1,1,0,
0,1,0,0,
0,0,0,0
}}
};
//构造函数
public Map(GameInfo gameInfo) {
this.gameInfo = gameInfo;
//实例化定时器
timer = new Timer(1000, new TimerListener());
init();
}
private void init() {
// TODO 游戏初始化
initMap();
drawWall();
music = new MusicUtil();
MusicUtil.ac.loop();
newBlock();
//开启定时器
timer.start();
}
private void newBlock() {
// TODO 随机下标值
// 判断是刚开始还是 已经开始一轮了
if (newType == -1) {
//游戏刚开始
blockType = r.nextInt(7); //0-6
turnState = r.nextInt(4);
newType = r.nextInt(7);
newState = r.nextInt(4); //0-3
}else {
//不是游戏刚开始,当前状态就是new出来的上一个状态
blockType = newType;
turnState = newState;
//新状态随机
newType = r.nextInt(7);
newState = r.nextInt(4); //0-3
}
//在右下的区域,显示下一次的方法
gameInfo.showNext(shapes[newType][newState]);
//规定新的方块生成的位置
x = 5;
y = 0;
//游戏结束
if(isGameOver) {
timer.stop();//停止计时器
MusicUtil.ac.stop();
music.gameOver();
judgeScore();
int index = JOptionPane.showConfirmDialog(this, "就这? 再来一局!","游戏结束",JOptionPane.YES_NO_CANCEL_OPTION);
if(index == 0) {
//再来一局
repaint();
init();
}else {
System.exit(1);
}
}
}
//判断 是否超过历史最高分
private void judgeScore() {
highestScore = new File("E:\\新建文件夹\\java\\javadata\\API\\src\\com\\yc\\Tetris\\images\\HigestScore.txt");
String str = "";
try {
BufferedInputStream is = new BufferedInputStream(new FileInputStream(highestScore));
byte[] bs = new byte[is.available()];
is.read(bs);
str = new String(bs);
System.out.println(str);
int temp = Integer.parseInt(str);
//读取的数据大于 最高分则替换
if(score > temp) {
String s = score + "";
BufferedOutputStream os = new BufferedOutputStream(new FileOutputStream(highestScore));
os.write(s.getBytes());
os.flush();
os.close();
}
is.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//初始化地图
private void initMap() {
MusicUtil.ac.loop();
showBestScore();
score = 0;
isGameOver =false;
gameInfo.setScore(score);
for(int i = 0;i < 22 ;i++) {
for(int j = 0; j < 14; j++) {
map[i][j] = 0;
}
}
}
//显示最高分
private void showBestScore() {
// TODO Auto-generated method stub
highestScore = new File("E:\\新建文件夹\\java\\javadata\\API\\src\\com\\yc\\Tetris\\images\\HigestScore.txt");
BufferedInputStream is;
try {
is = new BufferedInputStream(new FileInputStream(highestScore));
byte[] bs = new byte[is.available()];
is.read(bs);
String str = new String(bs);
int bestScore = Integer.parseInt(str);
gameInfo.setBestScore(bestScore);
System.out.println(str);
is.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 碰撞检测
* @param x 当前位置
* @param y
* @param blockType 方块类型
* @param turnState
*/
public boolean checkCrash(int x, int y,int blockType , int turnState) {
//首先循环方块
for(int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
// xy 和index反过来的
if(shapes[blockType][turnState][i * 4 + j] == 1 && map[y + i + 1][x + j] == 2) {
//当方块的值为1 才做碰撞检测 撞到墙
return true;
}
if(shapes[blockType][turnState][i * 4 + j] == 1 && map[y + i + 1][x + j] == 1) {
//当方块的值为1 才做碰撞检测 撞到墙
if( y + i <= 1) {
//游戏结束
isGameOver = true;
}
//撞到其他方块
return true;
}
}
}
//没有碰撞
return false;
}
public boolean checkCrashL(int x3 ,int y3 , int blockType, int turnState) {
//首先循环方块
for(int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
// xy 和index反过来的
if(shapes[blockType][turnState][i * 4 + j] == 1 && map[y + i][x + j - 1 ] == 2) {
//当方块的值为1 才做碰撞检测 撞到墙
return true;
}
if(shapes[blockType][turnState][i * 4 + j] == 1 && map[y + i ][x + j - 1 ] == 1) {
//当方块的值为1 才做碰撞检测 撞到墙
if( y + i <= 1) {
//游戏结束
isGameOver = true;
}
//撞到其他方块
return true;
}
}
}
//没有碰撞
return false;
}
public boolean checkCrashR(int x3 ,int y3 , int blockType, int turnState) {
//首先循环方块
for(int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
// xy 和index反过来的
if(shapes[blockType][turnState][i * 4 + j] == 1 && map[y + i][x + j + 1 ] == 2) {
//当方块的值为1 才做碰撞检测 撞到墙
return true;
}
if(shapes[blockType][turnState][i * 4 + j] == 1 && map[y + i ][x + j + 1 ] == 1) {
//当方块的值为1 才做碰撞检测 撞到墙
if( y + i <= 1) {
//游戏结束
isGameOver = true;
}
//撞到其他方块
return true;
}
}
}
//没有碰撞
return false;
}
/**
* 将 方块添加到map数组中去
* @param x2
* @param y2
* @param blockType2
* @param turnState2
*/
private void add(int x2, int y2, int blockType2, int turnState2) {
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if( y + i > 20 || x + j < 1 || x + j > 12) {
//边界不能替换
continue;
}else {
if(map[y + i ][x + j ]== 0) {
//地图为空
map[y + i][x + j] = shapes[blockType][turnState][i * 4 + j];
}
}
}
}
}
private void drawWall() {
// TODO 画墙
for(int i = 0; i < 14; i++) {
map[21][i] = 2;
}
for(int i = 0; i < 22; i++) {
map[i][0] = 2;
map[i][13] = 2;
}
}
//键盘触发的操作
public void up(){
int temp = turnState;
turnState = (turnState + 1) % 4;
if(checkCrash(x, y, blockType, turnState)) {
turnState = temp;
}
repaint();
}
public void left(){
if(! checkCrashL(x,y,blockType,turnState)) {
x--;
}
repaint();
}
public void right(){
if(! checkCrashR(x,y,blockType,turnState)) {
x++;
}
repaint();
}
public void down(){
if(! checkCrash(x,y,blockType,turnState)) {
y++;
}else {
add(x,y,blockType,turnState);
newBlock();
//判断能否消除
removeLine();
}
repaint();
}
private void removeLine() {
int count = 0;
for (int i = 0; i < 21; i++) {
for (int j = 1; j < 13; j++) {
if(map[i][j] == 1) {
count ++ ;
//将地图上面的图像整体往下移
if(count >= 12) {
for (int a = i;a > 1; a--) {
for(int b = 1; b < 13 ; b++) {
map[a][b] = map[a-1][b];
}
}
//每消除一行+ 10 分
score += 10;
gameInfo.setScore(score);
music.removeMusic();
if(score > 50 && score <100) {
timer.setDelay(700);
}else if (score > 100 && score <200 ) {
timer.setDelay(500);
}else if(score > 200 && score < 300){
timer.setDelay(300);
}else if(score > 300) {
timer.setDelay(100);
}
count = 0;
}
}else {
//中间有空的不能消除, 直接判断下一行,记得清零
count = 0;
break;
}
}
}
}
//重写绘图方法
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
//开始画黄色方块
for(int i = 0; i < 16; i++) {
//值为1,才画黄色方块
if( shapes[blockType][turnState][i] == 1) {
g.setColor(Color.orange);
//经典画矩形
g.fillRect((i % 4 + x) * 30, (i / 4 + y) * 30, 30, 30);
}
}
//开始画形状和边框
for(int i = 0; i < 22; i++) {
for(int j = 0; j < 14; j++) {
if(map[i][j]==1) {
//黄色区域
g.setColor(Color.orange);
g.fillRect(j * 30, i * 30, 30 , 30);
}else if(map[i][j] == 2) {
//黑色边框
g.setColor(Color.black);
g.fillRect(j * 30, i * 30, 30, 30);
}
}
}
//画线
g.setColor(Color.WHITE);
for(int i = 1; i < 22 ; i++) {
//0 30 - 420 30
//0 60 - 420 60
//0 90 - 420 90
g.drawLine(0, 30 * i, 420, 30 * i);
}
for(int i = 1; i < 14 ;i++) {
g.drawLine(i * 30, 0 , i * 30, 660);
}
}
//定时器的实现
class TimerListener implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
//重绘
repaint();
if(! checkCrash(x,y,blockType,turnState)) {
y++;
}else {
//将方块添加到地图中
add(x,y,blockType,turnState);
//判断能否消除
removeLine();
newBlock();
}
//repaint();
}
}
}
四、音乐类
播放背景音乐,消除音乐 ,游戏结束的音乐
package com.yc.Tetris;
import java.applet.Applet;
import java.applet.AudioClip;
import java.net.URL;
public class MusicUtil {
//开始播放背景音乐
static URL url = MusicUtil.class.getClass().getResource("/com/yc/Tetris/Music/4296.wav");
static AudioClip ac = Applet.newAudioClip(url);
public void removeMusic(){
URL url = MusicUtil.class.getClass().getResource("/com/yc/Tetris/Music/9297.wav");
AudioClip ac = Applet.newAudioClip(url);
//循环播放
ac.play();
}
public void gameOver() {
URL url = MusicUtil.class.getClass().getResource("/com/yc/Tetris/Music/gameover.wav");
AudioClip ac = Applet.newAudioClip(url);
//循环播放
ac.play();
}
}
五、测试类
游戏还做了最高分的记录,通过文件的操作实现的,在地图类里有写到
根据积分的增长,加快下降速度,也是一个简单的判断,通过计时器的setdelay()实现 有兴趣的可以 仔细看一看
package com.yc.Tetris;
import java.awt.BorderLayout;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JSplitPane;
public class Test {
public static int[][][] shapes = {
// 一
{ {
0,1,0,0,
0,1,0,0,
0,1,0,0,
0,1,0,0
},{
1,1,1,1,
0,0,0,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
0,1,0,0,
0,1,0,0,
0,1,0,0
},{
1,1,1,1,
0,0,0,0,
0,0,0,0,
0,0,0,0
} },
// 田
{ {
0,1,1,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,1,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,1,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,1,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
} },
// 7
{{
0,1,1,0,
0,0,1,0,
0,0,1,0,
0,0,0,0
},{
0,0,1,0,
1,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
0,1,0,0,
0,1,1,0,
0,0,0,0
},{
0,1,1,1,
0,1,0,0,
0,0,0,0,
0,0,0,0
}},
//反7
{{
0,1,1,0,
0,1,0,0,
0,1,0,0,
0,0,0,0
},{
1,1,1,0,
0,0,1,0,
0,0,0,0,
0,0,0,0
},{
0,0,1,0,
0,0,1,0,
0,1,1,0,
0,0,0,0
},{
0,1,0,0,
0,1,1,1,
0,0,0,0,
0,0,0,0
}},
//立
{{
0,1,0,0,
1,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
0,1,1,0,
0,1,0,0,
0,0,0,0
},{
1,1,1,0,
0,1,0,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
1,1,0,0,
0,1,0,0,
0,0,0,0
}},
// z
{{
0,0,1,1,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
0,1,1,0,
0,0,1,0,
0,0,0,0
},{
0,0,1,1,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,1,0,0,
0,1,1,0,
0,0,1,0,
0,0,0,0
}},
// 反z
{{
1,1,0,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,0,1,0,
0,1,1,0,
0,1,0,0,
0,0,0,0
},{
1,1,0,0,
0,1,1,0,
0,0,0,0,
0,0,0,0
},{
0,0,1,0,
0,1,1,0,
0,1,0,0,
0,0,0,0
}}
};
public static void main(String[] args) {
JFrame jf = new JFrame();
jf.setSize(696,696);
jf.setTitle("俄罗斯方块");
jf.setLocationRelativeTo(null);//居中显示
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setResizable(false);
//设置图标
jf.setIconImage(Toolkit.getDefaultToolkit().getImage(Test.class.getResource("/com/yc/Tetris/images/logo.png")) );
//实例化面板
//定义一个分割面板
JSplitPane jSplitpane = new JSplitPane();
//添加到JFrame里去
jf.add(jSplitpane,BorderLayout.CENTER);
//实例化两个面板
GameInfo gameinfo = new GameInfo();
Map cb = new Map(gameinfo);
//将左边的游戏区域,和右边的信息区域,添加到分割面板中
jSplitpane.setLeftComponent(cb);
jSplitpane.setRightComponent(gameinfo);
//最后设置距离
jSplitpane.setDividerLocation(420); //右边距离左边420
jSplitpane.setEnabled(false);//禁止拖动
//jf.add(gameinfo);
jf.setVisible(true);
//键盘监听事件
jf.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void keyReleased(KeyEvent e) {
// TODO Auto-generated method stub
}
@Override
public void keyPressed(KeyEvent e) {
// TODO 键盘事件
//获取按压的键盘对应 的数值
//System.out.println(e.getKeyCode());
int code = e.getKeyCode();
switch(code) {
case 37: //左
cb.left();
break;
case 38:
cb.up(); //上
break;
case 39: //→
cb.right();
break;
case 40: //下
cb.down();
break;
}
}
});
//调用一下方法
// int[] shape = {0,0,0,1,
// 0,1,1,1,
// 0,0,0,1,
// 0,0,0,0};
//
// gameinfo.showNext(shape);
}
}
六、源码链接
最后附上源码链接供大家下载参考
完全免费~
https://download.csdn.net/download/weixin_44420328/12648212