1,事件机制
在游戏中,用户可以进行很多不同的操作。
当用户按左键时,那么空白右边的9就需要往左移。
如图所示:
当用户按右键时,那么空白左边的4就需要往右移。
当用户按上键时,那么空白下面的5就需要往上移。
当用户按下键时,那么空白上面的8就需要往下移。
当用户点击了菜单里面的重新开始时,游戏里面的数字就需要重新打乱顺序。重开一局游戏。
当用户点击了联系我们的时候,就需要弹出一个黑马官方公众号的二维码
也就是说,当用户进行不同的操作,我们要执行的代码就不一样了。
所以,此时就需要有监听机制。监听机制可以监听到用户进行的不同操作。针对于不同的操作我们可以写不同的代码。而监听到用户的操作,也叫做事件。
在刚刚的分析中:用户在键盘上进行的按键操作,就是键盘事件。
用户点击不同的按钮,就是点击事件。
那么在代码中如何写呢?就要用到下面学习的接口了。
2,接口
- Java 接口是一系列方法的声明,是一些方法特征的集合。
- 用于制定 规则
2.1接口定义
我们可以定义两个方法,当键盘按下的时候,执行按下方法里面的代码。当键盘按键松开的时候。执行松开的代码。
public class KeyListener {
public void 按下(){
...
}
public void 松开(){
...
}
}
但是因为不同的项目中,所操作的业务逻辑是不一样的。所以这个方法里面的代码就是谁用谁写。
所以,在定义接口的时候,干脆里面的方法就不写大括号。那一旦不写大括号,上面的class也要变一下,变成interface。
所以,接口定义格式如下:
public interface KeyListener {
public void 按下();
public void 松开();
}
2.2接口使用
在使用一个接口时,可以定义一个类实现这个接口,并补全里面所有的方法。
此时,实现这个接口的类,叫做该接口的实现类。
代码如下:
public class MainFrame implements KeyListener {
public void 按下() {
...
}
public void 松开() {
...
}
}
此时,如果我想监听键盘按键的时候,就可以使用这个接口。
当键盘任意一个按键被按下的时候,会自动调用按下这个方法。
当键盘任意一个按键松开的时候,会自动调用松开这个方法。
2.3Java中的接口
这些接口,都不需要我们自己写。Java已经帮我们写好了。
如果我们想监听键盘事件,就实现KeyListener这个接口。
如果我们想监听游戏中哪个东西被点击了,就实现ActionListener接口。
不管实现哪个接口,接口里面所有的方法我们都要写一遍。
3,KeyListener接口
现在,我想按上下左右键之后,图片可以移动起来。那么此时必须要实现Java已经写好的KeyListener。
示例代码如下:
public class MyJFrame extends JFrame implements KeyListener {
...
}
然后把里面所有的方法自己补全。但是不需要自己手写,用鼠标点击一下红色波浪线,按下快捷键:alt+enter。这样就可以自动生成。
public class MyJFrame extends JFrame implements KeyListener {
...
@Override
public void keyTyped(KeyEvent e) {
...
}
//按下
@Override
public void keyPressed(KeyEvent e) {
...
}
//松开
@Override
public void keyReleased(KeyEvent e) {
...
}
}
其中有两个方法要知道,当键盘上按键被按下的时候,系统会自动调用keyPressed方法,当按键松开的时候,会自动调用keyReleased方法。所以我们可以在方法中分别写输出语句实现一下。
public class MyJFrame extends JFrame implements KeyListener {
...
@Override
public void keyTyped(KeyEvent e) {}
//按下
@Override
public void keyPressed(KeyEvent e) {
System.out.println("键盘上按键被按下了");
}
//松开
@Override
public void keyReleased(KeyEvent e) {
System.out.println("键盘上按键松开了");
}
}
最后还要让整个界面绑定一下键盘监听事件。
此时就相当于有一个人在监听界面的键盘事件,如果此时键盘按下了,则会自动调用对应的方法
public void initFrame() {
...
//让界面添加键盘监听
this.addKeyListener(this);
...
}
游戏启动后,如果没有按键盘上的键,那么这两个方法不会被调用。
如果我们按下任意一个键,会发现。控制台输出:
键盘上按键被按下了
键盘上按键松开了
这可以说明三件事情:
- 按键时,会有多个动作。先是按下动作,最后是松开动作。
- 按下按键时,keyPressed自动被调用。不需要我们自己调用。
- 松开按键时,keyReleased自动被调用。不需要我们自己调用。
细节:当我们要写代码实现按某一个键实现对应的逻辑时,一般会把代码写在松开的方法中。
4,键位与数字的对应关系
在刚刚我们已经监听到了键盘事件了。但是有一个问题,不管我们按下什么键,都会去自动调用keyPressed方法,不管我们松开什么键,都会自动调用keyReleased方法。
所以我们还要对按键进行一个判断。
在Java中,键盘上的每一个键位都会跟一个数字一一对应。
- 左键 — 37
- 上键 — 38
- 右键 — 39
- 下键 — 40
这些数字不需要我们记住。可以通过getKeyCode
方法获取到。
代码示例:
public class MyJFrame extends JFrame implements KeyListener {
...
@Override
public void keyTyped(KeyEvent e) {}
//按下
@Override
public void keyPressed(KeyEvent e) {
System.out.println("键盘上按键被按下了");
}
//松开
@Override
public void keyReleased(KeyEvent e) {
//获取按键对应的数字
int keyCode = e.getKeyCode();
//打印数字
System.out.println(keyCode);
System.out.println("键盘上按键松开了");
}
}
那么这样,我们通过对这些数字判断就可以实现移动效果了。
5,图片移动
咱们已经学习了事件机制的相关知识,那么下面我们就让图片动起来。
我们可以获取到按键对应的数字,然后对数字进行判断。
针对于不同的按键,执行不同的代码逻辑。
步骤:
1,在keyReleased方法中获取按键对应的数字
2,定义一个move方法。在move方法中对上下左右进行判断。
3,在keyReleased中调用move方法,并传递按键对应的数字。
4,移动之后要重新绘制整个游戏界面。
代码示例:
public class MyJFrame extends JFrame implements KeyListener {
...
@Override
public void keyTyped(KeyEvent e) {}
//松开
@Override
public void keyReleased(KeyEvent e) {
//获取按键的数字
int keyCode = e.getKeyCode();
//调用move方法并传递数字
move(keyCode);
}
//判断上下左右,来执行不同的代码
public void move(int keyCode) {
if (keyCode == 37) {
//左
//表示空格右边的图片要向左移动
datas[x0][y0] = datas[x0][y0 + 1];
datas[x0][y0 + 1] = 0;
y0++;
} else if (keyCode == 38) {
//上
//表示空格下面的图片要向上移动
datas[x0][y0] = datas[x0 + 1][y0];
datas[x0 + 1][y0] = 0;
x0++;
} else if (keyCode == 39) {
//右
//表示空格在左边的图片要向右移动
datas[x0][y0] = datas[x0][y0 - 1];
datas[x0][y0 - 1] = 0;
y0--;
} else if (keyCode == 40) {
//下
//表示空格在上面的图片要向下移动
datas[x0][y0] = datas[x0 - 1][y0];
datas[x0 - 1][y0] = 0;
x0--;
} else {
System.out.println("只能按上下左右");
}
}
public void initImage() {
//表示将界面中原来所有的图片全部删除
this.getContentPane().removeAll();
...//中间的代码不用动
//将整个界面重新绘制
this.getContentPane().repaint();
}
}
6,作弊码
到目前为止,游戏的主体功能都已经全部写完了。那么下面,我们再添加一些额外的小功能。首先是作弊码。
当我们在玩游戏的时候,觉得自己可能无法通关了。那么就可以按这个作弊码一键通关,惊呆你的小伙伴。
首先,咱们得定一个按键是作弊码。假设,我定w为作弊码。当我按下w的时候,就可以一键通关。
但是此时我还不知道w按键对应的数字怎么办啊?其实没关系,打印一下就可以了。
public class MyJFrame extends JFrame implements KeyListener {
...
@Override
public void keyTyped(KeyEvent e) {}
//松开
@Override
public void keyReleased(KeyEvent e) {
//获取按键的数字
int keyCode = e.getKeyCode();
//打印数字
System.out.println(keyCode);
//调用move方法并传递数字
move(keyCode);
}
...
}
我们可以在keyReleased方法中获取按键对应的数字,再把数字打印出来。启动游戏之后,我们按下w键。
控制台输出:
87
此时就证明,w按键对应的数字为87。
在move方法中,加上对87的判断就可以了。如果按下87的时候,直接将二维数组中的数字变成最终的数字。
代码示例:
public void move(int keyCode) {
...
} else if (keyCode == 87) {
//w 表示作弊码
datas = new int [][]{{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,0}};
}
...
}
7,判断胜利
现在游戏可以正常玩耍了,也可以用作弊码一键通关。但是有一个小问题。当游戏成功通关之后,根本没有胜利提示,这让我绞尽脑汁之后总感觉少了一点成就感(如下左图所示)。我想着如果游戏成功通关后,在界面上有成提示,那么玩起来也有一点动力。很多人玩单机游戏,都想看看通关之后的画面是什么样的。所以,我就要在通关之后,加上一个胜利标志(如下右图所示)。
其实胜利标志就是一张图片而已。在资料的image文件中有一张win.png。在游戏通关之后,把他显示出来即可。如下图所示:
所以,我们在每次绘制整个页面之前,都要判断一下二维数组中的数字是否已经排列成功,如果已经排列成功,那么就显示胜率图标,如果没有排列成功,就不显示胜利图标。
步骤:
1,定义一个方法victory,需要判断datas数组中数字是否排列成功,将判断结果返回。
2,对victory方法的结果进行判断。
3,如果为真,则加载胜利图标,如果为假,则不加载。
7.1带有返回值的方法
其中带有返回值的方法,跟之前学习的方法是一样的。
7.1.1定义格式:
public 返回值类型 方法名(){
//方法执行的代码
return xxx;
}
在以前的方法中,不需要方法的执行结果。所以我们一律写成public void。其中void表示该方法没有返回值。
现在我们要定义victory方法,在方法中要判断二维数组中的数据。所以需要把方法比较的结果返回给调用者。
7.1.2return关键字
victory方法中,用了一个关键字return。
return关键字的两个使用地点:
- 没有返回值的方法中的作用
结束方法 - 有返回值的方法中的作用
1,结束方法
2,后面紧跟方法的最终结果,将方法的最终结果返回给调用者。
注意点:
方法实际返回的值,要跟返回值类型保持一致即可。
举例:
如果返回整数,那么返回值类型就写int
如果返回小数,那么返回值就写double
如果返回字符串,那么返回值就写String
现在我返回一个判断结果,要么是true,要么是false。在Java中这叫做布尔类型,用boolean表示。
7.2代码实现
判断的二维数组元素的步骤如下:
1,在类的上面定义一个新的二维数组,该二维数组中的元素是按照正确方式排列的。
2,在victory方法中,对两个二维数组进行比较即可。
如果完全一致,那么返回true。
只要有一个不一样,那么返回false
3,在initImage方法中对victory的结果进行判断。
如果为真,表示游戏通关,显示胜利图标。
如果为假,表示游戏未通关,不显示胜利图标。
public class MyJFrame extends JFrame implements KeyListener {
...
//定义一个二维数组,元素是最终胜利的元素。
int [][] victory = {{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,0}};
public void initImage() {
...
//判断游戏是否胜利
if(victory()){
//如果该方法的结果是true
//那么就表示游戏胜利
ImageIcon imageIcon = new ImageIcon("image\\win.png");
JLabel jLabel = new JLabel(imageIcon);
jLabel.setBounds(514/2-266/2,230,266,88);
this.add(jLabel);
}
...
}
//判断两个数组中元素是否相同
public boolean victory(){
//datas 和 victory
for (int i = 0; i < datas.length; i++) {
for (int j = 0; j < datas[i].length; j++) {
if(datas[i][j] != victory[i][j]){
//return --- 可以写在方法当中
// 1,停止方法
// 2,将return后面的数据,返回给调用者。
return false;
}
}
}
return true;
}
...
}
8,计步功能
我们在玩游戏的时候,为了体现我们很厉害,通常都会看多少步完成了这个游戏。所以,我们也要加一个计步功能。
计步功能其实就是定义一个变量,每移动一步就自增一次。将这个变量的最新值显示在界面上就可以了。
实现步骤:
1,因为在多个方法中使用,所以在类的上面定义一个变量step
2,在move方法中,按上下左右的时候,按一次step需要++一次。
3,讲step显示在界面上。
public class MyJFrame extends JFrame implements KeyListener {
//第一步:统计走了多少步的变量
int step = 0;
//第二步:move方法中step自增。
public void move(int keyCode) {
if (keyCode == 37) {
//左
//表示空格右边的图片要向左移动
datas[x0][y0] = datas[x0][y0 + 1];
datas[x0][y0 + 1] = 0;
y0++;
step++;
} else if (keyCode == 38) {
//上
//表示空格下面的图片要向上移动
datas[x0][y0] = datas[x0 + 1][y0];
datas[x0 + 1][y0] = 0;
x0++;
step++;
} else if (keyCode == 39) {
//右
//表示空格在左边的图片要向右移动
datas[x0][y0] = datas[x0][y0 - 1];
datas[x0][y0 - 1] = 0;
y0--;
step++;
} else if (keyCode == 40) {
//下
//表示空格在上面的图片要向下移动
datas[x0][y0] = datas[x0 - 1][y0];
datas[x0 - 1][y0] = 0;
x0--;
step++;
} else if (keyCode == 87) {
//w 表示作弊码
datas = new int [][]{{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,0}};
}else {
System.out.println("只能按上下左右");
}
}
//第三步:将step的值显示到界面上
public void initImage() {
...
JLabel label_step = new JLabel("步数:" + step);
label_step.setBounds(50,20,100,20);
this.add(label_step);
...
}
}
9,重新开始
当一局游戏结束了,想重新开始玩第二局的时候,可以点击菜单里面的功能—重新游戏。
重新游戏,顾名思义,一切从头开始。
- 1-15张数字图片要重新打乱顺序。
- 计步器的值要清0。
9.1ActionListener接口
我们点击重新游戏按键是没有任何反应的,因为我们还没有监听他。
所以,我们还要学习一个接口,ActionListener接口。这个接口就可以监听用户按了游戏里面的哪些组件。
示例代码如下:
如果实现多个接口,那么接口之间要用逗号隔开。
public class MyJFrame extends JFrame implements KeyListener, ActionListener {
...
}
然后把里面所有的方法自己补全。但是不需要自己手写,用鼠标点击一下红色波浪线,按下快捷键:alt+enter。这样就可以自动生成。
public class MyJFrame extends JFrame implements KeyListener, ActionListener {
...
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("用户点击了重新开始");
}
}
最后,还要让重新开始这个jMenuItem的去绑定一下动作监听事件。
此时就相当于有一个人在监听重新开始这个jMenuItem的动作事件,如果这个条目被点击了,就会自动的调用对应的方法。
public void initMenu() {
jMenuItem1.addActionListener(this);
}
游戏启动后,只要用户点击菜单里面的重新游戏,那么都会自动调用actionPerformed方法。
方法中的代码就会自动执行。
此时控制台输出:
用户点击了重新开始
每点一下,都会输出一次。
9.2代码实现
在上面,我们已经实现了用户点击重新开始这个jMenuItem能自动的调用actionPerformed方法了。那么下面,我们只要把重新开始的逻辑代码写在actionPerformed方法中就可以了。
实现步骤:
1,重新打乱数据的顺序
2,计步变量清零
3,重新绘制整个界面
因为上面的三步我们都已经写在单独的方法中,所以此处不需要额外再写代码。直接调用对应的方法就可以了。
代码示例:
@Override
public void actionPerformed(ActionEvent e) {
//重新数据打乱数据的顺序
initData();
//计步的变量必须清零
step = 0;
//绘制整个界面
initImage();
}
10,联系我们
我们继续添加一个联系我们的功能,要把这个功能放到菜单当中。
所以,我们要在菜单的JMenuBar中再添加一个JMenu,在JMenu中添加一个JMenuItem即可。
为了让我们点击这个新加的有效果,所以也要让他去绑定一个动作事件。
当点击了联系我们时,会出现一个弹框。
10.1JDialog
弹框是通过JDialog实现的,我们可以把图片交给JDialog,当联系我们被点击时,JDialog自动弹出来,那么图片也就随之出现了。
实现步骤如下:
1,创建JDialog对象
2,创建ImageIcon对象
3,创建JLabel对象,并将ImageIcon交给JLabel
4,利用JLabel设置图片的大小位置。
5,把JLabel添加到JDialog中。
6,对JDialog进行一些设置。
代码示例:
//创建了一个弹框对象
JDialog jDialog = new JDialog();
//创建了一个图片对象
ImageIcon imageIcon = new ImageIcon("image\\about.png");
JLabel jLabel = new JLabel(imageIcon);
jLabel.setBounds(0,0,344,344);
//把图片放到弹框当中
jDialog.add(jLabel);
//给弹框设置大小
jDialog.setSize(344,344);
//要把弹框在设置为顶层 -- 置顶效果
jDialog.setAlwaysOnTop(true);
//要让jDialog居中
jDialog.setLocationRelativeTo(null);
//让jDialog显示出来
jDialog.setVisible(true);
10.2代码实现
我们已经学会了JDialog,他表示一个弹框,弹框里面可以放图片,可以放文字。
现在我们想出现一张黑马公众号二维码的图片,所以我们希望让JDialog弹出一张图片。
实现步骤如下:
1,创建JMenu(关于)
2,创建JMenuItem(联系我们)
3,给JMenuItem(联系我们)绑定一个动作事件。
当我们点击他时,可以自动调用actionPerformed方法。
4,在actionPerformed方法中添加判断,判断当前是点击什么。
5,如果点击的是联系我们,那么JDialog出现。
代码实现:
@Override
public void actionPerformed(ActionEvent e) {
//判断,
//如果点击的是重新开始,那么才执行下面的代码
//如果点击的是关于我们,弹出一个对话框
if(e.getSource() == jMenuItem1){
//重新数据打乱数据的顺序
initData();
//计步的变量必须清零
step = 0;
//绘制整个界面
initImage();
}else if(e.getSource() == jMenuItem2){
//点击了联系我们
System.out.println("点击了联系我们");
//创建了一个弹框对象
JDialog jDialog = new JDialog();
//创建了一个图片对象
ImageIcon imageIcon = new ImageIcon("image\\about.png");
JLabel jLabel = new JLabel(imageIcon);
jLabel.setBounds(0,0,344,344);
//把图片放到弹框当中
jDialog.add(jLabel);
//给弹框设置大小
jDialog.setSize(344,344);
//要把弹框在设置为顶层 -- 置顶效果
jDialog.setAlwaysOnTop(true);
//要让jDialog居中
jDialog.setLocationRelativeTo(null);
//让jDialog显示出来
jDialog.setVisible(true);
}else{
System.out.println("没有这个案按键");
}
}
11,最终优化
11.1分包
11.1.1作用
游戏的主体代码和一个额外的功能,我们已经写完了。写完之后发现,idea左侧的类太多了。我们想找非常的麻烦。其中只有两个类是跟游戏相关的,其他所有的类,都是学习一些知识点时新建的。
如下图所示:
所以,为了方便管理这些java文件,我们会再次新建文件夹。在Java中,文件夹也称之为包。针对不同功能的文件夹会新建不同的包来进行管理。
我们现在可以新建两个包,一个用来放游戏相关的,一个用来放学习时候用来演示的类。
11.1.2创建包的步骤:
- 右键点击src,选择new,再选择Package
- 给包起个名字,名字要见名知意。
- 新建两个包。
- 一个起名gamecode:里面存放跟游戏相关的两个java文件。
- 一个起名demo:里面都是讲解知识点时用来演示的Java文件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YW98CXJH-1646910262429)(img\新建包.png)]
11.2游戏异常
到目前为止,游戏还剩下最后一个问题。
如下图所示:
在控制台上出现了很多红色文字,但是游戏在玩的时候不受影响。
出现了红色文字,代表我们代码出现了异常,也就是某些地方写错了。
这个问题在四种情况下会出现。
- 当空白区域出现在最右侧时,再按左键。
- 当空白区域出现在最左侧时,再按右键。
- 当空白区域出现在最上侧时,再按下键。
- 当空白区域出现在最下侧时,再按上键。
这是为什么呢?其实很简单,我们以图中的情况为例。
此时,当空白区域出现在最右侧时,再按左键,控制台出现异常。
那是因为,按下左键时,需要把空白区域右侧的数字图片左移,而图中空白区域右侧没有数字图片可以移动了,所以出错了。同理,其他三种情况也是一样的。没有东西可以移动了,你还硬要让他移,肯定就出错了。
所以,我们就要针对这四种情况进行优化。
- 当空白区域出现在最右侧时,再按左键时,不再移动。
- 当空白区域出现在最左侧时,再按右键时,不再移动。
- 当空白区域出现在最上侧时,再按下键时,不再移动。
- 当空白区域出现在最下侧时,再按上键时,不再移动。
代码示例:
public void move(int keyCode) {
if (keyCode == 37) {
//左
//表示空格右边的图片要向左移动
if(y0 == 3){
//1,结束方法
//2,如果我要把方法的结果进行返回,那么可以在return写上对应的值。
return;
}
datas[x0][y0] = datas[x0][y0 + 1];
datas[x0][y0 + 1] = 0;
y0++;
step++;
} else if (keyCode == 38) {
if(x0 == 3){
return;
}
//上
//表示空格下面的图片要向上移动
datas[x0][y0] = datas[x0 + 1][y0];
datas[x0 + 1][y0] = 0;
x0++;
step++;
} else if (keyCode == 39) {
if(y0 == 0){
return;
}
//右
//表示空格在左边的图片要向右移动
datas[x0][y0] = datas[x0][y0 - 1];
datas[x0][y0 - 1] = 0;
y0--;
step++;
} else if (keyCode == 40) {
if(x0 == 0){
return;
}
//下
//表示空格在上面的图片要向下移动
datas[x0][y0] = datas[x0 - 1][y0];
datas[x0 - 1][y0] = 0;
x0--;
step++;
} else if (keyCode == 87) {
//w 表示作弊码
datas = new int [][]{{1,2,3,4},{5,6,7,8},{9,10,11,12},{13,14,15,0}};
}else {
System.out.println("只能按上下左右");
}
}