此项目为java基础的阶段项目,此项目涉及了基础语法,面向对象等知识,具体像语法基础如判断,循环,数组,字符串,集合等…; 面向对象如封装,继承,多态,抽象类,接口,内部类等等…都有涉及。此项目涉及的内容比较多,作为初学者可以很好的将前面的知识串起来。此项目拿来练手以及回顾前面的基础知识是个很不错的选择
注意:这个素材里面的每张图片被分成了15份,每张图片都是正方形的,并且长和宽都是105像素
链接:https://pan.baidu.com/s/16maOd105xKqrWZSd6IScig?pwd=1234
提取码:1234
游戏窗口我们主要用到Java给我提供的JFrame类,这个类可以给我们提供一个窗口,我们可以对这个窗口设置大小,位置,以及是否显示等等…,下面是界面的样子
我们后面所有的业务逻辑都要在这种窗口里面实现,下面是实现的代码:
JFrame jf = new JFrame();
jf.setSize(400, 400);
jf.setVisible(true);
JFrame是Java给我提供的一个类,所以我们可以直接用JFrame来创建对象,里面的setSize()方法就是给窗口设置大小,单位都是像素; 这里需要注意的是,不是设置完大小执行就可以出现,JFrame默认隐藏了界面窗口,我们需要用setVisible()方法将他展示出来,true就是展示,相反false就是隐藏句号
在游戏里我们需要三个界面来展示,分别是:游戏界面, 登录界面, 注册界面
所以我们需要创建三个JFrame对象,这里我们需要想一个问题:我们新建一个类文件,然后在main方法里创建三个JFrame对象,然后把所有业务逻辑全部写在main方法里吗? 显而易见结果是NO,大NO特NO,到时候一千多行代码全写在一个main方法里吗,到时候出BUG了,在哪都不知道。所以我建议分三个类,即游戏界面, 登录界面, 注册界面这三个类,然后我们在新建一个类用main方法来执行我们的游戏,在main方法里创建三个窗口类的对象用来调用方法,比如设置窗口大小等等。这样我们就可以需要在那个窗口做业务逻辑就直接去相应的类里去做就可以了。大致思路如下:
这是游戏程序执行入口:
import ui.GameFrame;
import ui.LoginJFrame;
import ui.RegisterJFrame;
public class App {
public static void main(String[] args) {
//表示程序的启动入口
new GameFrame();
new LoginJFrame();
new RegisterJFrame();
}
}
下面是GameFrame(游戏界面)类
这里我用了一个空参构造来进行窗口大小以及展示界面的设置,方便在main方法里我们创建GameFrame对象时,就会调用GameFrame空参构造来设置界面,这样就不用我们在main方法里去设置
package ui;
import javax.swing.*;
public class GameFrame extends JFrame {
public GameFrame(){
this.setSize(603, 680);
this.setVisible(true);
}
}
下面是LoginFrame(登录界面)类
思路和GameFrame(游戏界面)类一样
package ui;
import javax.swing.*;
public class LoginJFrame extends JFrame {
public LoginJFrame(){
this.setSize(488, 430);
this.setVisible(true);
}
}
下面是RegisterFrame(注册界面)类
思路也和GameFrame(游戏界面)类一样
package ui;
import javax.swing.*;
public class RegisterJFrame extends JFrame {
public RegisterJFrame(){
this.setSize(488, 500);
this.setVisible(true);
}
}
大致框架搭建好之后,这样我们只用在对应的类中进行相应的业务逻辑的搭建就好了,我们后面的所有业务逻辑的实现几乎都在这三个窗口中。
我们将游戏窗口显示出来后,还需要对游戏窗口进行相应的设置,以至于能给我们更好的游戏体验和视觉效果,接下来我们需要对游戏界面设置以下功能和代码:
1.游戏标题
游戏标题就是我们游戏的名字,一般写在游戏界面的左上方,效果如下图
标题设置用到了JFrame的方法,代码如下:
//设置界面的标题
this.setTitle("拼图游戏单机版 V1.0");
2.游戏主界面置顶
置顶功能想必大家应该很了解了,就是当我们打开其他软件的时候,置顶能够让我们在点击其他应用的时候依然能够显示在最上层,这里我们会用到JFrame里的一个方法
//设置界面置顶
//盖住其他所有软件
this.setAlwaysOnTop(true);
3.游戏主界面居中
游戏界面居中就是当我们打开游戏的时候,界面始终是在我们电脑屏幕的正中间出现,方法如下:
//设置界面居中
this.setLocationRelativeTo(null);
由于这个方法需要传递一个component参数,这里我们只用填一个null就可以了
4.设置游戏关闭模式
游戏关闭模式,JFrame给我们提供了关闭方法
//设置游戏的关闭模式
this.setDefaultCloseOperation(3);
这个方法可以传递一个int类型的参数,这个方法有四种关闭模式,分别对应数字0, 1, 2, 3; 四种关闭模式,可以进入这个方法的源码进行查看,这里我只说数字3对应的关闭模式: 就是当我们关闭其中一个窗口,虚拟机就会停止运行,可以理解为,我只要关闭其中一个游戏窗口,那么其他登录窗口和注册窗口都会关闭,并且虚拟机会停止运行
注意: this代表这个所在的类,由于前面我们将三个窗口类都继承了JFrame,所以我们可以直接用this调用父类方法
一个游戏必不可少的就是菜单,我们重新游戏,重新登录等等关于游戏的都会在菜单中展示出来,如下图就是此游戏菜单的样式
在我们创建这样的菜单之前,我们必须先了解Java给我们提供的JMenuBar类,JMenu类和JMenuItem类,下面我会用几张图让你了解这三种类有什么关系
了解完这三种类之后,我们需要将这三种类联系起来然后放进GameFrame,下面是实现的步骤:
1.先创建JMenuBar
2.再创建JMenu
3.再创建JMenuItem
4.把JMenuItem放到JMenu里面
5.把JMenu放到JMenuBar
6.最后把JMenuBar添加到整个JFrame界面中就可以了
下面是代码实现:
//初始化菜单
public void initJMenuBar(){
//创建整个的菜单对象
JMenuBar jMenuBar = new JMenuBar();
//创建菜单上面的两个选项的对象(功能 关于我们)
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
//创建选项下面的条目对象
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
//将每一个选项下的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
//将菜单里面的两个选项添加到菜单中
jMenuBar.add(functionJMenu);
jMenuBar.add(aboutJMenu);
//给整个界面设置菜单
this.setJMenuBar(jMenuBar);
}
这里我把初始化菜单构造成了一个方法,然后直接在GameFrame空参构造里面调用就可以实现下面效果:
你也发挥想象添加其他菜单到菜单栏中
下面的是我目前GameFrame类的结构和代码
public class GameFrame extends JFrame {
public GameFrame(){
//初始化界面
initJFrame();
//初始化菜单
initJMenuBar();
//让界面显示出来,建议放到最后
this.setVisible(true);
}
//初始化界面
public void initJFrame(){
//设置界面的宽高
this.setSize(603, 680);
//设置界面的标题
this.setTitle("拼图游戏单机版 V1.0");
//设置界面置顶
//盖住其他所有软件
this.setAlwaysOnTop(true);
//设置界面居中
this.setLocationRelativeTo(null);
//设置游戏的关闭模式
this.setDefaultCloseOperation(3);
}
//初始化菜单
public void initJMenuBar(){
//创建整个的菜单对象
JMenuBar jMenuBar = new JMenuBar();
//创建菜单上面的两个选项的对象(功能 关于我们)
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
//创建选项下面的条目对象
JMenuItem replayItem = new JMenuItem("重新游戏");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
//将每一个选项下的条目添加到选项当中
functionJMenu.add(replayItem);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
//将菜单里面的两个选项添加到菜单中
jMenuBar.add(functionJMenu);
jMenuBar.add(aboutJMenu);
//给整个界面设置菜单
this.setJMenuBar(jMenuBar);
}
}
窗口和菜单大致搭建完之后,我们现在可以把图片试着添加到主窗口中了,由于游戏窗口都是GameFrame窗口内,所以后面的业务逻辑也都是在GameFrame类中完成的
添加图片之前,我们重新了了解一下游戏窗口
这里的游戏窗口实际上是分为三部分的
1.标题部分
2.菜单部分
3.隐藏容器部分
这里的隐藏容器部分是当我们创建JFrame对象的时候,已经存在的,所以我们不需要重新创建对象,我们可以直接用this.getContentPane()方法调用就可以了,再用它去调用add()方法添加图片进去就可以了,如果没有特殊要求,它会默认的放置在正中央,如果我们想要放置其他位置,就必须要把其默认开关给关闭,关闭我们可以用下面的方法
//取消默认的居中放置,只有取消了才会按照XY轴的形式添加组件
this.setLayout(null);
由于隐藏容器是在创建JFrame对象的时候产生的,所以我们可以在initFrame()即初始化界面里使用这个setLayout方法,后面我们就可以将图片以XY轴的方式放置在隐藏容器中,而不会默认放置在正中间
当我们取消了隐藏容器默认居中的方式之后,接下来我们就可以添加图片了
在JFrame中添加图片,不是简单的将图片地址直接放入JFrame中,而是要用到ImageIcon和JLabel类
下面是实现步骤
1.我们要先创建一个ImageIcon对象,就相当于我们要放置的图片,传递的参数可以是图片地址
2.创建一个JLabel类来存储我们的ImageIcon对象也就是我们的图片,这个JLabel是一个管理容器容器,可以存放图片和文字等
3.用JLabel类创建的对象来指定图片的位置,也就是XY轴,这里我们会使用JLabel的setBounds方法,这里我们传递的参数是(x, y, width, height)也就是设置了图片的XY轴和宽高,这里的XY轴不是直角坐标系那个,在JFrame窗口里原点在左上角,下面的图可以让你更好理解
4.最后再把JLabel创建的对象添加到JFrame的隐藏容器中
下面我们就用代码来实现上面的功能
//初始化图片
public void initImage(){
//创建一个图片ImageIcon对象
ImageIcon icon = new ImageIcon("D:\\Java Code\\puzzlegame\\image\\animal\\animal3\\1.jpg");
//创建一个JLabel的对象(管理容器)
JLabel jLabel = new JLabel(icon);
//指定图片位置
jLabel.setBounds(0, 0 , 105, 105);
//把管理容器添加到界面中
this.getContentPane().add(jLabel);
}
这里我是在GameFrame类里构建了一个方法,然后在GameFrame类的空参构造里调用
这样写完之后,得到的效果应该是这样的
由于我们设置的setBounds方法里面的参数x = 0 , y = 0; 也就是放置在原点位置,这里需要注意我们是看图片左上角对应的坐标,也就是图片的坐标是图片左上角对应的坐标,很显然结果是对的
当我们知道了如何添加图片到JFrame中后,我们接下来添加其他图片
我知道你在想什么,打住!!! , 你不会真想把上面的代码写上十几遍就能添加所有图片了吧
答案是NO!!!
如果你真想尝试一下也不是不行,当你添加了3个图片进去的时候,这个时候你看代码,有没有发现什么规律,为了让大家更好看清,这里展示我添加了三个图片后的代码
//创建一个JLabel的对象(管理容器)放入ImageIcon对象
JLabel jLabel1 = new JLabel(new ImageIcon("D:\\Java Code\\puzzlegame\\image\\animal\\animal3\\1.jpg"));
//指定图片位置
jLabel1.setBounds(0, 0, 105, 105);
//把管理容器添加到界面中
this.getContentPane().add(jLabel1);
//创建一个JLabel的对象(管理容器)放入ImageIcon对象
JLabel jLabel2 = new JLabel(new ImageIcon("D:\\Java Code\\puzzlegame\\image\\animal\\animal3\\2.jpg"));
//指定图片位置
jLabel2.setBounds(105, 0, 105, 105);
//把管理容器添加到界面中
this.getContentPane().add(jLabel2);
//创建一个JLabel的对象(管理容器)放入ImageIcon对象
JLabel jLabel3 = new JLabel(new ImageIcon("D:\\Java Code\\puzzlegame\\image\\animal\\animal3\\3.jpg"));
//指定图片位置
jLabel3.setBounds(210, 0, 105, 105);
//把管理容器添加到界面中
this.getContentPane().add(jLabel3);
这里我图方便,我直接把创建的ImageIcon对象放入了创建JLabel对象的参数中
这里需要我们仔细观察这三段代码,告诉我有没有发现什么规律?
我们可以发现每次添加图片就只有图片位置发生了改变和图片地址发生了改变,尤其是图片位置改变的时候还满足一个规律,就是每行图片的x轴都是逐张加105,也就是说第一行第一张x是0,第二张就是105,第三张就是210…以此类推,第一行的第n张的x就是105 * (n - 1), y始终都是0; 那第二行呢,和第一行一样,就是y变成了105, 第三行的y就是210…以此类推,第n行的y就是105 * (n - 1);
有了上面的思路我们现在就应该要想到循环了,因为我们是四行四列,我们就可以用行循环四次,再用列循环四次,这样我们可以用到循环嵌套,一个外循环一个内循环来添加图片,下面是代码演示:
public void initImage(){
int number = 1;
//外循环 --- 四行
for (int i = 0; i < 4; i++) {
//内循环 --- 一行的四张图片
for (int j = 0; j < 4; j++) {
//创建一个图片ImageIcon对象
//创建一个JLabel的对象(管理容器)放入ImageIcon对象
JLabel jLabel = new JLabel(new ImageIcon("D:\\Java Code\\puzzlegame\\image\\animal\\animal3\\" + number + ".jpg"));
//指定图片位置
jLabel.setBounds(105 * j, 105 * i, 105, 105);
//把管理容器添加到界面中
this.getContentPane().add(jLabel);
//number自增表示图片变化
number++;
}
}
}
这个功能我写在了GameFrame类里的initImage()方法里,然后在空参构造里调用
在上面的代码可以看到,外循环我用来表示行数,内循环表示列数,这个嵌套循环大致意思就是当我开始循环是i = 0表示第一行,然后 j 表示列数,j 逐渐增加, 表示列数逐渐增加, 表现在setBounds方法里的x逐渐增加,并且是逐级增加105, y又恰好是0, 刚好满足我们的图片放置规律, 第一行图片添加完之后, i 自增表示第二行,然后又执行内循环, 添加第二行的4张图片,然后继续循环知道图片所有图片添加完成
有人就会问了那个number是什么意思,这里我又要提醒一下了,图片的名字我们为了添加方便尽量设置成下面这种形式
注意:这里我将一个图片分成了15份来放入JFrame中,不能将一整个图片放入
这里我们在创建ImageIcon对象传递地址的时候就可以用一个数字逐渐递增来表示所有图片,并且将所有图片添加到容器中
这样就可以理解了number的含义了,就是作为地址值,添加对应的图片到容器中,number写进地址的时候,要记得写 “+number+”,不要直接将number写进去了,这样我们就可以得到下面的结果了
最后一张图片没有是因为我将一张图片分成了15份,当循环执行到 i = 3, j = 3 的时候没有相应的图片了,就展示了一张空白图,后面我们需要一个空白的位置来进行移动图片的操作
既然我们已经弄懂了如何在JFrame添加图片,接下来我们要做的事就是打乱图片顺序,毕竟是拼图游戏,本身就是一个打乱顺序的图片让我们去移动恢复的
要想打乱图片顺序,我们前面用了一个int类型的number变量来不断自增,我们的图片就依次放进去了,当然我们图片名得设置的有规律,既然我们的图片是按number的值来添加的,并且number的值是从1按顺序加到16,那么我们如果打破这个number顺序,是不是就能打破图片的顺序呢.接下来我们就尝试一下
要想打乱1-16的顺序,最简单的办法是设置一个长度为16的一维数组,然后进行索引的交换来打断顺序,下面就是实现的代码
//创建一个一维数组
int[] Arr = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
Random r = new Random();
//遍历数组,将索引进行交换
for (int i = 0; i < Arr.length; i++) {
int index = r.nextInt(Arr.length);
int temp = Arr[i];
Arr[i] = Arr[index];
Arr[index] = temp;
}
//遍历一维数组
for (int i = 0; i < Arr.length; i++) {
System.out.print(Arr[i] + " ");
}
这里需要注意的是,一维数组我为什么不按1-16,而是按0-15,是因为我的图片名字只有1-15,而多的那个0,也就是当识别到0的时候,在素材里找不到会自动空着,也就是空白图片,就是我们移动图片时候的白框
最后输出的结果:
很明显,顺序被完全打乱了,打乱完顺序之后,是不是就可以直接放入初始化图片方法里了呢,不不不,我们还要把图片索引弄的更优雅一点,既然拼图是按4 × 4 来显示,那我们的索引为什么不也按4 × 4 来显示呢
4 × 4 的索引排列第一个想到的应该是二维数组,那么现在我们只需要将一维数组转换成二维数组就可以了,我们可以用循环嵌套来表示,并逐渐将一维数组内的元素添加进去,下面是实现的代码:
//将一维数组添加到二维数组中
int[][] newArr = new int[4][4];
int index = 0;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
newArr[i][j] = Arr[index];
index++;
}
}
//遍历二维数组
for (int i = 0; i < newArr.length; i++) {
for (int j = 0; j < newArr[i].length; j++) {
System.out.print(newArr[i][j] + " ");
}
System.out.println();
}
最后输出的结果(一维数组和二维数组)
最后我们只用把这个二维数组的数据代入到初始化图片方法里就可以了
注意:二维数组的创建最好写在GameFrame类的成员位置,这里我为了方便展示就写在了初始化数据方法里,因为我们在初始化图片方法里需要用的这个二维数组,如果写在成员位置,我们就可以直接使用了,所以这个初始化数据方法相当于给二维数组赋值,初始化图片方法直接使用二维数组里的数据就可以了
下面是如何在初始化图片方法里使用二维数组来打乱图片的顺序
//外循环 --- 四行
for (int i = 0; i < 4; i++) {
//内循环 --- 一行的四张图片
for (int j = 0; j < 4; j++) {
//获取加载当前图片的序号
int num = newArr[i][j];
//创建一个图片ImageIcon对象
//创建一个JLabel的对象(管理容器)放入ImageIcon对象
JLabel jLabel = new JLabel(new ImageIcon("D:\\Java Code\\puzzlegame\\image\\animal\\animal3\\" + num + ".jpg"));
//指定图片位置
jLabel.setBounds(105 * j, 105 * i, 105, 105);
//把管理容器添加到界面中
this.getContentPane().add(jLabel);
}
}
我们之前用的是number来调用图片地址,而现在就是用二维数组里被打乱的值来调用,恰好这里有个循环嵌套,我们就可以直接用num来取出二维数组的值,这也就是为什么我前面要把一维数组转换成二维数组
最后的结果就是这样
结果确实符合我们的预期,和之前哈士奇的图片相比完全被打乱了
在我们打乱图片顺序之后,我们可以来着手美化界面了,我们主要美化游戏界面也就是添加一个游戏背景图,再给图片添加边跨,以及把图片和背景图移到中下的位置(多次尝试,中下的位置最好看)
1.移动图片的位置
//指定图片位置
jLabel.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
上面是添加图片时嵌套循环内的代码,也就是调整图片的位置,你们可以自己调试最好看的位置,我选择的是整体向右移动83像素,即x+83; 再整体向下移动134个像素,即y+134的位置最好看
2.添加游戏背景图
游戏背景图的添加依然是用图片添加的方法,但是有一点非常重要:
先添加的图片在上方,后添加的图片在下方,可能会与其他语言相反,这里要注意一下,所以添加背景图片的代码要写在添加图片的循环之后,并且在initImage()方法之内,下面是代码实现:
//添加背景图片
//创建ImageIcon对象
ImageIcon bg = new ImageIcon("image\\background.png");
//创建JLabel容器对象
JLabel backgound = new JLabel(bg);
backgound.setBounds(40,40,508,560);
//把背景图片添加到界面当中
this.getContentPane().add(backgound);
背景图片经过尝试,放在x = 40, y = 40的位置最好看
3.添加图片边框
添加图片边框不用我们自己找边框添加进去,JLabel也就是存放图片,文字的容器给我们提供了setBorder()方法,我们直接拿来用就好了
下面的代码是写在添加图片的嵌套循环内的
JLabel jLabel = new JLabel(new ImageIcon("image\\animal\\animal3\\" + num + ".jpg"));
//指定图片位置
jLabel.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
//给图片添加边框
//0:让图片凸起来
//1:让图片凹下去
jLabel.setBorder(new BevelBorder(1));
//把管理容器添加到界面中
this.getContentPane().add(jLabel);
setBorder()方法可以传递一个 BevelBorder的对象,里面的参数有两种选择,一个是0,一个是1, 0是可以添加让图片凸起来的边框, 1是可以添加让图片凹下去的边框,你们自己可以尝试一下,我个人觉得凹下去要好看一点
把上面的美化代码完成后,结果就是下面图片的样子
比之前还是好看多了
事件监听简单点理解就是java识别你在鼠标和键盘上做出了各种操作来做出回应, 就像这个游戏,我们按↑键,空白下面的图片就会往上移,像这样的,而java识别你按下了↑键然后来做出空白下面的图片上移就叫事件监听
关于事件监听,java给了我们三个接口,可以直接使用,分别是ActionListener(动作监听), MouseListener(鼠标监听), KeyListener(键盘监听), 其中动作监听是其他两个的简化版只能识别鼠标的左键点击和空格的键盘, 鼠标监听能识别单击, 按住不松, 松开, 划入, 划出, 键盘监听能识别按住不松, 键入, 松开, 这方面的内容你们可以查阅一下资料, 这里我不详细讲述, 这里我们只用到键盘监听的松开
下面我们就开始实践
由于事件监听是接口,我们直接在GameFrame类调用接口
public class GameFrame extends JFrame implements KeyListener
调用接口后,要重写接口所有的方法,一共有三个方法:
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
}
这里我们主要用到keyReleased方法,也就是松开, 也就是当我按下松开后才会识别, 我按住不松是不会识别的
我们移动图片的原理简单一点理解其实就是空白图片和四周图片进行移动,如何移动呢?
我们之间添加图片的时候,是按照索引来添加图片的,那我们移动图片其实也是根据索引来移动的,只要我们把移动之后的数据给初始化图片方法就可以实现移动图片, 所以我们大致实现的逻辑是
1.获取空白图片的位置
下面是初始化数据方法里将一维数组添加到二维数组的代码,这里我们主要就是判断当索引为0的时候,也就是空白图片的时候,就将它的索引获取出来,我在成员位置处设置了一个x 和 y来记录空白图片位置(由于键盘监听重写的方法里也要用到x和y),这里的x和y其实就是我们之前设置二维数组的 i 和 j ,如果你把它列成一个4 × 4 的表,它其实就是行和列
//将一维数组添加到二维数组
int index = 0;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if(Arr[index] == 0){
x = i;
y = j;
}else{
newArr[i][j] = Arr[index];
}
index++;
}
}
2.在keyReleased方法里实现移动逻辑
图片移动的原理其实就是空白图片与四周图片进行交换,前面我们知道了空白图片的位置,现在我们主要就是实现交换
这里的int code = e.getKeyCode(); 是接口提供的一种识别按键的方法, 它会将键盘几乎所有的按键命名,名字是int类型的数字,所以后面我们直接判断这个code,然后进行相应的交换就行,交换就是索引的交换, 比如说向下移,就是空白的图片与上面的图片交换, 空白图片向上移,空白图片的x不变, 空白图片的y就要 - 1, 被交换的图片向下移, 也就是被交换的图片x不变, y要+ 1; 被交换的图片位置由于和空白图片交换了,所以直接给它的索引设置为0就可以了, 最后不要忘了, 空白图片移动之后,位置也改变了, 比如向下移动,就是空白图片向上移动, 所以要x-- , x和y要始终表示空白图片的位置
@Override
public void keyReleased(KeyEvent e) {
//对上,下,左,右进行判断
//左:37, 右:39, 上:38, 下:40
int code = e.getKeyCode();
if(code == 37){
System.out.println("向左移动");
if(y == 3){
return;
}
newArr[x][y] = newArr[x][y + 1];
newArr[x][y + 1] = 0;
y++;
//调用方法按最新的方法加载图片
initImage();
}else if(code == 38){
System.out.println("向上移动");
if(x == 3){
//表示方块已经已经在最下方了,他的下面没有图片再能移动了
return;
}
//把空白方块下方的数字赋值给空白方块
newArr[x][y] = newArr[x + 1][y];
newArr[x + 1][y] = 0;
x++;
//调用方法按最新的方法加载图片
initImage();
}else if(code == 39){
System.out.println("向右移动");
if(y == 0){
return;
}
newArr[x][y] = newArr[x][y - 1];
newArr[x][y - 1] = 0;
y--;
//调用方法按最新的方法加载图片
initImage();
}else if(code == 40){
System.out.println("向下移动");
if(x == 0){
return;
}
newArr[x][y] = newArr[x - 1][y];
newArr[x - 1][y] = 0;
x--;
//调用方法按最新的方法加载图片
initImage();
}
}
3.移动之后的效果显示及刷新
在我们完成上面的代码后,执行会发现并不会移动,难道是写错了,其实不是,我之前说过了,我们在修改索引之后,调用了initImage方法,此时的移动之后的效果会重新放在主界面上,但是,它会被第一次初始化图片给覆盖,也就是先添加的图片在上方,后添加的图片在下方,千万不要忘记了,所以我们要在initImage方法的前面实现一个清空之前主界面的内容,这样每次移动就会显示移动后的内容
//清空原本已经出现的所有图片
//清空以后才会出现移动后的图片,不然被覆盖在下面了
this.getContentPane().removeAll();
上面这段代码要写在initImage()里,还要写在最前面
然后就是进行刷新,刷新可以写在initImage()方法的最下面
//刷新界面
this.getContentPane().repaint();
4.BUG修复
上面代码全写完,其实就已经可以游玩了,但是,游玩的过程中我们发现当空白图片在最上方的时候,我们按下↓键,程序会抛出异常,虽然可以正常游玩,但是还是有点不舒服,那是怎么回事呢?
其实就是我们在移动图片的时候没有考虑空白图片的位置,如果它在边界,如果我们执意让它往边界外移动的话,它肯定不能移动,这时就会抛出异常,其实也很好修复,只用在keyReleased方法里的四个按键判断语句中加上判断x和y是否在边界,如果在就不执行移动代码,直接退出就可以了,实现代码在2.在keyReleased方法里实现移动逻辑中
在我们玩游戏的时候,有时候会不知道游戏的完整图片是什么而不知道怎么移动拼图,所以下面我们就要实现按下快捷键出现完整图片的功能,我们拿按下A键举例,当我们按下A键不松的时候,游戏主界面会出现当前拼图的完整图片,送开就会重新显示我们游玩的界面。下面是大致思路:
1.键盘监听
既然是按下A键显示,那么我们肯定要用到键盘监听,之前我们实现移动键盘的逻辑的时候,已经重写了键盘监听的所有方法,所以我们可以直接用到重写的方法
2.按下不松
我们需要按下A键不松来显示完整图片,松开就会返回游戏界面,这里我们需要在按下keyPressed()和keyReleased()方法里书写代码,当我们按下就会执行keyPressed()方法里的内容,当我们松开就会执行keyReleased()方法里的内容
3.清除界面和显示图片
我们实现按下显示完整图片的原理就是,当我们按下的时候,我们游戏的图片就会全部清楚,并且将完整图片显示在主界面里,当我们松开后直接调用initImage()方法就可以显示我们之前的游戏内容
下面是代码实现
@Override
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();
if(code == 65){
//把界面中的所有图片全部清除
this.getContentPane().removeAll();
//加载第一张完整的图片
JLabel all = new JLabel(new ImageIcon("image\\animal\\animal3\\all.jpg"));
all.setBounds(83, 134, 420, 420);
this.getContentPane().add(all);
//添加背景图片
JLabel background = new JLabel(new ImageIcon("image\\background.png"));
background.setBounds(40,40,508,560);
//把背景图片添加到界面当中
this.getContentPane().add(background);
//刷新界面
this.getContentPane().repaint();
}
}
上面这段是识别我们按下A键的时候,清除所有图片的代码,并且刷新
else if(code == 65){
initImage();
上面这段是在keyReleased()方法内的else if() 因为之前判断按键移动图片的时候加上了判断,所以现在直接在后面写else if()就可以了, 然后直接调用initImage()方法, 这个方法会清除之前的所有内容并重新显示游戏图片和背景图
我们可以发现我们用的图片都是固定的,路径是固定的,不会改变,换图片也需要重写类里的所有路径,非常麻烦,我们现在就来优化一下,让我们以后更改的时候更加方便,我想的是在类的成员位置处加上一个字符串类型的path变量,然后在后面需要用到路径的方法里直接调用path,后面修改的时候,直接修改path就可以了
下面是代码实现
String path = "image\\animal\\animal3\\";
然后在需要路径的地方直接改成path就可以了
JLabel jLabel = new JLabel(new ImageIcon(path + num + ".jpg"));
JLabel all = new JLabel(new ImageIcon(path + "all.jpg"));
这样以后要修改图片直接修改path就可以了
这里的作弊码大家懂的都懂,我就不细说了,这里直接上代码
else if(code == 87){
//作弊码
//重写给二维数组赋值,初始化二维数组
newArr = new int[][]{
{1,2,3,4},
{5,6,7,8},
{9,10,11,12},
{13,14,15,0},
};
//调用上面的二维数组进行初始化图片,直接通关
initImage();
}
依旧是在keyReleased()方法里使用else if(), 这里我设置的是W键(对应87), 我们直接初始化二维数组,也就是按照通关的顺序设置二维数组, 然后直接调用initImage()方法就可以显示通关后的样子,如下图
判断胜利其实和作弊码很像,就是识别你当前的二维数组数据是不是和完整图片的数据一样,也就是和作弊码那个二维数组一样,一样就显示胜利图标,不一样就继续
1.定义一个正确的二维数组win,并设置在成员位置处,因为我们需要在多个方法里用到这个数据
//定义一个二维数组,存储正确的数据
int[][] win = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12},
{13,14,15,0}
};
2.在加载图片之前,先判断一下二维数组中的数字跟win数组中是否相同, 因为加载图片,显示游戏界面的功能都在initImage()方法里, 我们要想继续游戏,也就是继续显示游戏界面, 就要先识别有没有胜利
if(victory()){
//显示胜利的图标
JLabel winJLabel = new JLabel(new ImageIcon("D:\\Java Code\\puzzlegame\\image\\win.png"));
winJLabel.setBounds(203, 283, 197, 73);
this.getContentPane().add(winJLabel);
}
3.如果胜利了,就直接展示正确的图标,反之展示不正确的图标, 没有胜利会继续执行initImage()方法内的代码就是显示游戏拼图
4.BUG修复。当我们全部写好后,我们发现当我们胜利后,显示了正确的图标,但是图片仍可以移动,我想当我成功后,游戏就不能动了,除非我们退出或下一局。
这个就需要我们在keyReleased()方法里进行判断,如果我们胜利了,就直接退出该方法,退出后就表示我们之前判断按键的代码都不能执行,也就是图片不会再进行移动
//判断游戏是否胜利,如果胜利,此方法需要直接结束,不能再执行下面的移动代码了
if(victory()){
//结束方法
return;
}
上面这段代码要写在keyReleased()方法的最上面, 因为此方法下面就是我们判断上下左右按键的代码, 想要胜利后, 这些按键都不能使用, 就要在它们上面进行判断,如果胜利就直接结束方法, 不再执行下面的判断按键, 如果没有胜利就会接着执行下面的判断,直至胜利
为了增加游戏的趣味性, 在游戏的旁边,我们可以加上一个计数器来显示步数,每移动一次就加一, 这样我们就可以来比较谁的步数低,还是非常有意思的
统计步数逻辑的实现比较简单,我们可以在成员位置处定义一个计数器变量,然后在键盘监听的keyReleased()方法里的判断移动键的时候加上计数器变量自增,最后在initImage()方法里显示计数器内容就可以了,下面是代码实现:
//定义变量用来统计步数
int step = 0;
在成员位置处定义计算器变量
//添加计数器到主界面中
JLabel stepCount = new JLabel("步数:" + step);
stepCount.setBounds(50, 30, 100, 20);
this.getContentPane().add(stepCount);
在initImage()方法里实现显示计数器的功能
最后在判断上下左右键的代码最下面加上step++就可以实现每移动一步,游戏界面的左上角显示移动的步数, 如下图
重新游戏需要在菜单栏中点击重新游戏后,会重新打乱图片顺序,重新开始
既然是点击菜单栏中的数据, 那我们需要给这些菜单中的数据加上监听事件,最简单的就是给它们加上ActionListener,只有鼠标点击和键盘空格, 所以我们要在initJMenuBar()方法里给这些条目绑定事件监听
在调用ActionListener之前,我们要先调用ActionListene接口
public class GameFrame extends JFrame implements KeyListener, ActionListener
然后给条目加上事件
//给条目绑定事件
replayItem.addActionListener(this);
reLoginItem.addActionListener(this);
closeItem.addActionListener(this);
accountItem.addActionListener(this);
注意在我们重写ActionListener接口方法之前,我们要先把之前定义的条目对象即JMenuItem移动到成员位置处,因为我们需要在ActionListener接口重写方法内调用条目对象
下面是ActionListener接口重写方法内内容
这里判断当我们点击重新游戏的条目对象replayItem时,执行的代码
@Override
public void actionPerformed(ActionEvent e) {
//获取当前被点击的条目对象
Object obj = e.getSource();
//判断
if(obj == replayItem){
System.out.println("重新游戏");
//计数器清零
step = 0;
//再次打乱二维数组中的数据
initData();
//重新加载图片
initImage();
这里需要注意的是,计数器清零一定要放在初始化数据和初始化图片的上面,因为初始化图片里会用到计数器step, 如果不在上面清零, 就会造成计数器沿用上一次游戏的计数器数据
重新登录是我们点击菜单栏中的重新登录即可,由于我们前面已经给重新登录添加了事件监听, 所以我们只用在ActionListener接口重写的方法内再进行判断就可以了,下面是代码实现
else if(obj == reLoginItem){
System.out.println("重新登录");
//关闭当前的游戏界面
this.setVisible(false);
//打开登录界面
new LoginJFrame();
关闭游戏很简单,在重写方法内判断是否点击关闭游戏, 如果点击了直接退出虚拟机就可以了
else if(obj == closeItem){
System.out.println("关闭游戏");
System.exit(0);
关于我们,我们可以显示我们的二维码,就是当玩家点击关于我们的时候, 会弹出一个弹框显示我们的二维码图片, 弹框是一个新的界面,要重新对其设置大小, 居中, 置顶, 是否显示等等, 和JFrame差不多的性质。 下面是代码实现
else if(obj == accountItem){
System.out.println("公众号");
//创建一个弹框对象
JDialog jDialog = new JDialog();
//创建一个管理图片的容器对象JLabel
JLabel jLabel = new JLabel(new ImageIcon("image\\about.png"));
//设置位置和宽高
jLabel.setBounds(0,0,258,258);
//把图片添加到弹框当中
jDialog.getContentPane().add(jLabel);
//给弹框设置大小
jDialog.setSize(344, 344);
//让弹框置顶
jDialog.setAlwaysOnTop(true);
//让弹框居中
jDialog.setLocationRelativeTo(null);
//弹框不关闭则无法操作下面的界面
jDialog.setModal(true);
//让弹框显示出来
jDialog.setVisible(true);
}
游戏的大致内容我们就写完了, 为了增加游戏趣味性, 我们可以多添加几张图片, 并给图片进行分类, 玩家可以选择图片, 比如美女, 动物, 运动的图片, 我给的素材里面有好几种, 这种效果在我们实现前面的初始化图片后, 就比较简单了, 我们可以把重新游戏菜单再添加几个选项, 美女, 动物, 运动等等, 然后点击相应的选项就会随机抽取相应的图片进行游戏, 步数重置为0, 图片随机
实现的大致过程:
1.重新定义一个JMenu名为重新游戏, 然后添加图片类型的JMenuItem到JMenu(重新游戏)中, 最后把JMenu添加到JMenu(功能)中就可以实现多级菜单了
2.给图片类型添加动作监听事件, 判断点击的按钮, 对对应的按钮实现相应的操作, 比如我点美女, 此时会随机从美女图片中选取一张, 初始化数据, 初始化图片以及刷新, 这时, 我们之前定义的path变量就用上了, 只要在判断中修改path的值, 也就是图片地址, 就可以选择图片类型了
3.在初始化数据和初始化图片之前给step定义为0, 使步数清0
这里我没有写登录界面和注册界面, 感兴趣的可以自己研究一下, 代码比较多, 可以自己试着写一下, 你们也可以自己添加有趣的玩法, 使游戏更加多样化
package ui;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.xml.crypto.Data;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Random;
public class GameFrame extends JFrame implements KeyListener, ActionListener {
//JFrame 界面,窗体
//子类也可以表示界面和窗体
//那么以后GameFrame就表示游戏的主界面
//以后跟游戏相关的所有逻辑都写在这个类中
Random r = new Random();
//创建选项下面的条目对象
JMenuItem girlItem = new JMenuItem("美女");
JMenuItem animalItem = new JMenuItem("动物");
JMenuItem sportItem = new JMenuItem("运动");
JMenuItem reLoginItem = new JMenuItem("重新登录");
JMenuItem closeItem = new JMenuItem("关闭游戏");
JMenuItem accountItem = new JMenuItem("公众号");
//定义变量用来统计步数
int step = 0;
//定义一个二维数组,存储正确的数据
int[][] win = {
{1,2,3,4},
{5,6,7,8},
{9,10,11,12},
{13,14,15,0}
};
String path = "image\\animal\\animal3\\";
//记录空白方块在数组中的位置
int x = 0;
int y = 0;
//创建一个二维数组供初始化数据方法添加数据,和供初始化图片方法使用
int[][] newArr = new int[4][4];
public GameFrame(){
//初始化界面
initJFrame();
//初始化菜单
initJMenuBar();
//初始化数据(打乱图片顺序的数据)
initData();
//初始化图片
initImage();
//让界面显示出来,建议放到最后
this.setVisible(true);
}
//初始化界面
private void initJFrame(){
//设置界面的宽高
this.setSize(603, 680);
//设置界面的标题
this.setTitle("拼图游戏单机版 V1.0");
//设置界面置顶
//盖住其他所有软件
this.setAlwaysOnTop(true);
//设置界面居中
this.setLocationRelativeTo(null);
//设置游戏的关闭模式
this.setDefaultCloseOperation(3);
//取消默认的居中放置,只有取消了才会按照XY轴的形式添加组件
this.setLayout(null);
//给整个界面添加键盘监听事件
this.addKeyListener(this);
}
//初始化菜单
private void initJMenuBar(){
//创建整个的菜单对象
JMenuBar jMenuBar = new JMenuBar();
//创建菜单上面的两个选项的对象(功能 关于我们)
JMenu functionJMenu = new JMenu("功能");
JMenu aboutJMenu = new JMenu("关于我们");
JMenu replayJMenu = new JMenu("重新游戏");
//将图片类型添加到重新游戏菜单中
replayJMenu.add(girlItem);
replayJMenu.add(animalItem);
replayJMenu.add(sportItem);
//将每一个选项下的条目添加到选项当中
functionJMenu.add(replayJMenu);
functionJMenu.add(reLoginItem);
functionJMenu.add(closeItem);
aboutJMenu.add(accountItem);
//给条目绑定事件
reLoginItem.addActionListener(this);
closeItem.addActionListener(this);
accountItem.addActionListener(this);
girlItem.addActionListener(this);
animalItem.addActionListener(this);
sportItem.addActionListener(this);
//将菜单里面的两个选项添加到菜单中
jMenuBar.add(functionJMenu);
jMenuBar.add(aboutJMenu);
//给整个界面设置菜单
this.setJMenuBar(jMenuBar);
}
//初始化图片
//添加图片的时候,需要按照二维数组中管理的数据添加图片
private void initImage(){
//清空原本已经出现的所有图片
//清空以后才会出现移动后的图片,不然被覆盖在下面了
this.getContentPane().removeAll();
if(victory()){
//显示胜利的图标
JLabel winJLabel = new JLabel(new ImageIcon("D:\\Java Code\\puzzlegame\\image\\win.png"));
winJLabel.setBounds(203, 283, 197, 73);
this.getContentPane().add(winJLabel);
}
//添加计数器到主界面中
JLabel stepCount = new JLabel("步数:" + step);
stepCount.setBounds(50, 30, 100, 20);
this.getContentPane().add(stepCount);
//添加背景图片
//先加载的图片在上方,后加载的图片在下方
//外循环 --- 四行
for (int i = 0; i < 4; i++) {
//内循环 --- 一行的四张图片
for (int j = 0; j < 4; j++) {
//获取加载当前图片的序号
int num = newArr[i][j];
//创建一个图片ImageIcon对象
//创建一个JLabel的对象(管理容器)放入ImageIcon对象
JLabel jLabel = new JLabel(new ImageIcon(path + num + ".jpg"));
//指定图片位置
jLabel.setBounds(105 * j + 83, 105 * i + 134, 105, 105);
//给图片添加边框
//0:让图片凸起来
//1:让图片凹下去
jLabel.setBorder(new BevelBorder(1));
//把管理容器添加到界面中
this.getContentPane().add(jLabel);
}
}
//添加背景图片
//创建ImageIcon对象
ImageIcon bg = new ImageIcon("image\\background.png");
//创建JLabel容器对象
JLabel backgound = new JLabel(bg);
backgound.setBounds(40,40,508,560);
//把背景图片添加到界面当中
this.getContentPane().add(backgound);
//刷新界面
this.getContentPane().repaint();
}
//初始化数据
private void initData(){
//打乱一个数组里数字的顺序,并构成二维数组
int[] Arr = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
Random r = new Random();
//遍历数组进行交换
for (int i = 0; i < Arr.length; i++) {
int index = r.nextInt(Arr.length);
int temp = Arr[i];
Arr[i] = Arr[index];
Arr[index] = temp;
}
//将一维数组添加到二维数组
int index = 0;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if(Arr[index] == 0){
x = i;
y = j;
}
newArr[i][j] = Arr[index];
index++;
}
}
}
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();
if(code == 65){
//把界面中的所有图片全部清除
this.getContentPane().removeAll();
//加载第一张完整的图片
JLabel all = new JLabel(new ImageIcon(path + "all.jpg"));
all.setBounds(83, 134, 420, 420);
this.getContentPane().add(all);
//添加背景图片
JLabel background = new JLabel(new ImageIcon("image\\background.png"));
background.setBounds(40,40,508,560);
//把背景图片添加到界面当中
this.getContentPane().add(background);
//刷新界面
this.getContentPane().repaint();
}
}
@Override
public void keyReleased(KeyEvent e) {
//判断游戏是否胜利,如果胜利,此方法需要直接结束,不能再执行下面的移动代码了
if(victory()){
//结束方法
return;
}
//对上,下,左,右进行判断
//左:37, 右:39, 上:38, 下:40
int code = e.getKeyCode();
if(code == 37){
System.out.println("向左移动");
if(y == 3){
return;
}
newArr[x][y] = newArr[x][y + 1];
newArr[x][y + 1] = 0;
y++;
//每移动一次,计数器自增一次
step++;
//调用方法按最新的方法加载图片
initImage();
}else if(code == 38){
System.out.println("向上移动");
if(x == 3){
//表示方块已经已经在最下方了,他的下面没有图片再能移动了
return;
}
//逻辑:
//把空白方块下方的数字往上移动
//x , y 表示空白方块
//x+1 , y表示空白下方的方块
//把空白方块下方的数字赋值给空白方块
newArr[x][y] = newArr[x + 1][y];
newArr[x + 1][y] = 0;
x++;
//每移动一次,计数器自增一次
step++;
//调用方法按最新的方法加载图片
initImage();
}else if(code == 39){
System.out.println("向右移动");
if(y == 0){
return;
}
newArr[x][y] = newArr[x][y - 1];
newArr[x][y - 1] = 0;
y--;
//每移动一次,计数器自增一次
step++;
//调用方法按最新的方法加载图片
initImage();
}else if(code == 40){
System.out.println("向下移动");
if(x == 0){
return;
}
//逻辑
//把空白方块往下移
newArr[x][y] = newArr[x - 1][y];
newArr[x - 1][y] = 0;
x--;
//每移动一次,计数器自增一次
step++;
//调用方法按最新的方法加载图片
initImage();
}else if(code == 65){
initImage();
}else if(code == 87){
//作弊码
//重写给二维数组赋值,初始化二维数组
newArr = new int[][]{
{1,2,3,4},
{5,6,7,8},
{9,10,11,12},
{13,14,15,0},
};
//调用上面的二维数组进行初始化图片,直接通关
x = 3;
y = 3;
initImage();
}
}
//判断data数组中的数据是否跟win数组中相同
//如果全部相同,返回true,否则返回false
public boolean victory(){
for (int i = 0; i < newArr.length; i++) {
for (int j = 0; j < newArr[i].length; j++) {
if(newArr[i][j] != win[i][j]){
//只要有一个数据不一样则返回false
return false;
}
}
}
//循环结束表示数组遍历比较完毕,完全一样则返回true
return true;
};
@Override
public void actionPerformed(ActionEvent e) {
//获取当前被点击的条目对象
Object obj = e.getSource();
//判断
if(obj == reLoginItem){
System.out.println("重新登录");
//关闭当前的游戏界面
this.setVisible(false);
//打开登录界面
new LoginJFrame();
}else if(obj == closeItem){
System.out.println("关闭游戏");
System.exit(0);
}else if(obj == accountItem){
System.out.println("公众号");
//创建一个弹框对象
JDialog jDialog = new JDialog();
//创建一个管理图片的容器对象JLabel
JLabel jLabel = new JLabel(new ImageIcon("image\\about.png"));
//设置位置和宽高
jLabel.setBounds(0,0,258,258);
//把图片添加到弹框当中
jDialog.getContentPane().add(jLabel);
//给弹框设置大小
jDialog.setSize(344, 344);
//让弹框置顶
jDialog.setAlwaysOnTop(true);
//让弹框居中
jDialog.setLocationRelativeTo(null);
//弹框不关闭则无法操作下面的界面
jDialog.setModal(true);
//让弹框显示出来
jDialog.setVisible(true);
}else if(obj == girlItem){
System.out.println("美女图片");
//随机获取图片
int num = r.nextInt(1, 14);
path = "image\\girl\\girl"+ num + "\\";
//初始化步数
step = 0;
//初始化数据
initData();
//初始化图片
initImage();
//刷新
this.repaint();
}else if(obj == animalItem){
System.out.println("动物图片");
int num = r.nextInt(1, 9);
path = "image\\animal\\animal"+ num + "\\";
//初始化步数
step = 0;
//初始化数据
initData();
//初始化图片
initImage();
//刷新
this.repaint();
}else if(obj == sportItem){
System.out.println("运动图片");
int num = r.nextInt(1, 11);
path = "image\\sport\\sport"+ num + "\\";
//初始化步数
step = 0;
//初始化数据
initData();
//初始化图片
initImage();
//刷新
this.repaint();
}
}
}
下面是主程序
import ui.GameFrame;
import ui.LoginJFrame;
import ui.RegisterJFrame;
public class App {
public static void main(String[] args) {
//表示程序的启动入口
//如果我们想要开启一个界面,就创建谁的对象就可以了
new GameFrame();
//new LoginJFrame();
//new RegisterJFrame();
//
}
}