本文介绍用swing编写连连看的基本思路,代码是交叉编的,写的太零碎的话不方便拷贝运行就写一起了。
程序组成:UI 基本数据 算法
一、数据
常量数据部分:
1.玩过连连看的都知道,连连看基本都是网格结构,每个格子里面都存放着图片、位置和图片存在对应关系,适合这个顺序结构的适合用数组来处理。所以我们需要一个数组来存放图片,数组的下标与该数组元素 的图片的这种对应关系很重要!
2.我们为了方便修改数组的大小来方便以后定义游戏难度什么的,需要将数组的大小设置一个常量。
3.图片的大小这个经常会用也设置成常量。
**在写算法的时候我们需要一些队列来保存一些需要跨类使用的点。
为方便使用,我们将这些常量定义成接口,方便使用,代码如下:
package fly;
import java.awt.Point;
import java.util.ArrayList;
import javax.swing.ImageIcon;
/**
* 用来定义常量的接口
*/
public interface Config {
// 游戏中可能使用到的数据
// 开始放置图片的左上角的位置
public static final int X0 = 60;
public static final int Y0 = 30;
// 图片的大小
public static final int SIZE = 30;
// 总行数和列数
public static final int ROWS = 8;
public static final int COLS = 12;
// 定义一个数组来存放需要显示在面板上的所有图片
public static ImageIcon[][] ICONS = new ImageIcon[ROWS][COLS];
//定义一个数组用来保存被消除的格子的中心点坐标和转折点的坐标
//当消除的时候可以使用连线提示被消除的格子经过的线路
public static ArrayList<Point> list = new ArrayList<Point>();
//选择的时候的提示框
public static ArrayList<Point> list2 = new ArrayList<Point>();
}
二、UI制作
UI分为图片、面板、窗体这三部分
窗体好解决用JFrame添加一些组件,做好布局即可;面板差不多使用JPanel,不过可能要重写paint方法。解决好图片的摆放、绘制问题
这个UI就好办了。
关于图片有一个不好处理的地方:1、要保证随机的每种图片是偶数张。
解决这个问题有2个办法:a.由系统自由随机图片 然后检查图片是否能保证偶数张。
b.一次随机得到图片后将图片保存2次,再随机位置
由于a方法可能造成程序死循环、效率比较低,我们一般采用b方法。
代码如下:
//装载图片
public void initPicData() {
// 用来存储图片的临时队列
ArrayList<ImageIcon> iconList = new ArrayList<ImageIcon>();
// 定义随机数对象,随机图片
Random rd = new Random();
// 循环所需图片个数的一半,每随机一张图片,就往队列中放两次
// 结果是:队列中的图片总数就是所需要的图片数,保证每张图片都是偶数个
for (int i = 0; i < ROWS * COLS / 2; i++) {
int num = rd.nextInt(10);
// 拼接图片路径字符串(我的图片都是0.gif这种用数字命名的方便随机)
String url = "imgs/" + num + ".gif";
// 创建图片对象
ImageIcon icon = new ImageIcon(url);
// 将图片放入队列中,每张图片装两次
iconList.add(icon);
iconList.add(icon);
}
// 将队列中的图片随机取出,放入二维数组
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
// 从队列中随机取一个下标
int index = rd.nextInt(iconList.size());
// 根据下标取得队列中的一张图片
// remove方法是取出并移除该下标位置的元素
ICONS[i][j] = iconList.remove(index);
}
}
}
注意:由于我们的游戏面板是动态变化的,需要不停刷新,所以我们需要重写JPanel的paint方法!
class MyPanel extends JPanel {
@Override
public void paint(Graphics g) {
super.paint(g);
// 循环取出二维数组中的图片,绘制出来
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
ImageIcon img = ICONS[i][j];
if (img != null) {
// 计算图片的下标
g.drawImage(img.getImage(), X0 + SIZE * j, Y0 + SIZE
* i, SIZE, SIZE, null);
}
}
}
}
}
由于我们是通过鼠标点击面板玩游戏,所以需要给面板添加鼠标监听器。
代码如下:
// 绘窗体
public void initUI() {
// 设置窗体标题
this.setTitle("连连看");
// 设置窗体大小
this.setSize(600, 400);
// 设置窗体不可更改大小
this.setResizable(false);
// 设置关闭窗体后结束窗体这个线程
this.setDefaultCloseOperation(3);
// 添加游戏区域面板(这个Mypanel后面会有)
JPanel gamePanel = new MyPanel();
gamePanel.setBackground(Color.BLACK);
// 将面板添加到窗体
this.add(gamePanel);
// 设置窗体可见
this.setVisible(true);
// 得到画布,Graphics2D有比Graphics更多的方法
Graphics2D g = (Graphics2D) gamePanel.getGraphics();
// 创建鼠标监听器
LLKListener lis = new LLKListener(g);
// 给面板添加鼠标监听器
gamePanel.addMouseListener(lis);
}
然后在添加上main方法
public static void main(String[] args) {
//创建窗体对象
LlkFrame llk = new LlkFrame();
// 调用方法初始化数据
//装载图片
llk.initPicData();
//绘制窗体、面板
llk.initUI();
}
查看已经初始化的界面,可以发现图片完全满足要求
三、算法部分
用来处理监听器传来的鼠标对应的位置的数组下标判断两个图片是否能够消除。
这里我们主要考虑3种情况:
A.2张图片不转折直接就可以消除,这又有2种情况,a.2张图片同一行时;b.2张图片同一列时
代码如下:
// 检测一行是否连通
public static boolean checkRow(int r1, int c1, int r2, int c2) {
// 只考虑同行的情况
if (r1 == r2) {
int min = Math.min(c1, c2);
int max = Math.max(c1, c2);
// 不要包含c1,c2两个位置
for (int i = min + 1; i < max; i++) {
if (ICONS[r1][i] != null) {
return false;
}
}
list.add(new Point(r1, c1));
list.add(new Point(r2, c2));
// 如果for循环执行完了,还没有false,则证明中间都是null
return true;
} else {
return false;
}
}
// 检测一列是否连通
public static boolean checkCol(int r1, int c1, int r2, int c2) {
if (c1 == c2) {
int min = Math.min(r1, r2);
int max = Math.max(r1, r2);
for (int i = min + 1; i < max; i++) {
if (ICONS[i][c1] != null) {
return false;
}
}
list.add(new Point(r1, c1));
list.add(new Point(r2, c2));
return true;
} else {
return false;
}
}
B.2张图片通过线段连线转折一次直接就可以消除,这又有2种情况,a.转折点为转折点为r1c2;b 转折点为r2c1
代码如下:
// 有一个转折点的
public static boolean onePoint(int r1, int c1, int r2, int c2) {
// 转折点为r1c2
if (ICONS[r1][c2] == null) {
if (checkRow(r1, c1, r1, c2) && checkCol(r2, c2, r1, c2)) {
return true;
}
}
// 转折点为r2c1
if (ICONS[r2][c1] == null) {
if (checkRow(r2, c2, r2, c1) && checkCol(r1, c1, r2, c1)) {
return true;
}
}
return false;
}
C.2张图片通过线段连线转折二次直接就可以消除,这又有2种情况,a.先往水平方向连线,恰好2个转折点在同列;
b.先往垂直方向连线,恰好2个转折点在同一行
代码如下:
// 水平移动
public static boolean twoPointH(int r1, int c1, int r2, int c2) {
// 定义一个变量表示两个转折点的列下标
for (int i = 0; i < COLS; i++) {
// 转折点为r1,i和r2,i
if (ICONS[r1][i] == null && ICONS[r2][i] == null) {
if (checkRow(r1, i, r1, c1) && checkRow(r2, i, r2, c2)
&& checkCol(r1, i, r2, i)) {
return true;
}
}
}
return false;
}
// 垂直移动
public static boolean twoPointV(int r1, int c1, int r2, int c2) {
for (int i = 0; i < ROWS; i++) {
if (ICONS[i][c1] == null && ICONS[i][c2] == null) {
if (checkRow(i, c1, i, c2) && checkCol(r1, c1, i, c1)
&& checkCol(r2, c2, i, c2)) {
return true;
}
}
}
return false;
}
四、监听器部分
我们需要通过鼠标点击玩游戏,做一些动态交互。主要是得到鼠标点的坐标后做一系列的判断操作。
将鼠标所在位置对应图片:
private ImageIcon getIconByLocation(int x, int y) {
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
// 获得该图片左上角的坐标
int x1 = X0 + SIZE * j;
int y1 = Y0 + SIZE * i;
// 如果在范围内,表示当次选中的是,i,j位置的数据
if (x - x1 < SIZE && y - y1 < SIZE) {
// 取出数组中ij位置的数据
ImageIcon icon = ICONS[i][j];
return icon;
}
}
}
return null;
}
要想在连连看的面板上绘图需要从外部传入画布(一般通过构造方法最方便)
public LLKListener(Graphics2D g) {
this.g = g;
// 设置线条的粗细
this.g.setStroke(new BasicStroke(5));
this.g.setColor(Color.RED);
}
监听器mouseReleased方法(用来得到鼠标释放时所在的点,并进行相应处理)
public void mouseReleased(MouseEvent e) {
// 获得事件源对象:产生事件的对象,即为游戏面板
JPanel panel = (JPanel) e.getSource();
// 获得光标按下的位置
int x = e.getX();
int y = e.getY();
// 保存坐标,用于绘制提示框
int r = (y - Y0) / SIZE;
int c = (x - X0) / SIZE;
int xp = X0 + c * SIZE;
int yp = Y0 + r * SIZE;
//将这个点添加进提示框点的队列中
list2.add(new Point(xp, yp));
//在点击的点所在的图片位置绘制矩形
g.drawRect(xp, yp, SIZE, SIZE);
if (count == 0) {
icon1 = getIconByLocation(x, y);
// 根据坐标计算下标
i1 = (y - Y0) / SIZE;
j1 = (x - X0) / SIZE;
System.out.println(i1 + " " + j1);
count++;
} else {
icon2 = getIconByLocation(x, y);
i2 = (y - Y0) / SIZE;
j2 = (x - X0) / SIZE;
// 当第二次点击完毕,就开始判断两次选中的图片是不是一样的
// System.out.println(icon1 + "<>" + icon2);
// 都不为空,且不是同一个位置,且图片名称相同
if (icon1 != null && icon2 != null && (i1 != i2 || j1 != j2)
&& icon1.toString().equals(icon2.toString())) {
if (SuanFa.checkRow(i1, j1, i2, j2)
|| SuanFa.checkCol(i1, j1, i2, j2)
|| SuanFa.onePoint(i1, j1, i2, j2)
|| SuanFa.twoPointH(i1, j1, i2, j2)
|| SuanFa.twoPointV(i1, j1, i2, j2)) {
// 先绘制提示线
for (int i = 0; i < list.size(); i += 2) {
Point p1 = list.get(i);
Point p2 = list.get(i + 1);
// 将下标转成坐标
int x1 = X0 + p1.y * SIZE + SIZE / 2;
int y1 = Y0 + p1.x * SIZE + SIZE / 2;
int x2 = X0 + p2.y * SIZE + SIZE / 2;
int y2 = Y0 + p2.x * SIZE + SIZE / 2;
g.drawLine(x1, y1, x2, y2);
}
// 清空队列
list.clear();
//添加一下延迟,方便查看画出来的线条
try {
Thread.sleep(1000);
} catch (Exception e1) {
e1.printStackTrace();
}
// g.draw
ICONS[i1][j1] = null;
ICONS[i2][j2] = null;
System.out.println("相同,可消除");
}
}
// 刷新一下
panel.repaint();
count--;
}
}