一、移动过程分析
问题:你觉得当你按了向上的按钮后,它该怎么移动才是一个合理的业务逻辑呢?
正常的业务逻辑应该是将空白的图片往下方移动。
在这我们就可以得出一个结论:向上移动实际上就是把空白方块下方的图片网上移动。
逻辑我们知道了,那代码该如何实现呢?
我们知道,在代码中,每一张小图片其实都跟一个唯一的数字对应,而这些数字都是存放在二维数组中的。
因此我们只需要找到 0
和 13
在二维数组中的位置就行了,然后再做一个数据的交换。
现在,我又要问大家了:你觉得 0
和 13
在二维数组中的索引分别是多少呢?
正确答案是:0
—— (1, 1)
;13
—— (2, 1)
因此,网上移动的本质其实就是:将 13
复制给 (1, 1)
位置,然后再把 (2, 1)
位置变成 0
就行了。
交换完成后,根据数字去添加对应的图片就行了,这就是向上移动。
为了让大家更好的理解数字在二维数组中的位置,下图给每个数字都做了索引标记。
二、给界面添加点击事件
找到 GameJFrame类
(游戏的主界面),首先我们要做的其实并不是 上下左右移动
,而是需要先给整个界面添加一个键盘监听事件。
因为我们是在整个游戏界面中按 上下左右键
的时候才会去移动图片。
我们让 GameJFrame类
去 implements KeyListener
,然后重写里面所有的抽象方法。
此时我们需要找到 initJFrame()
初始化界面的方法,在方法的最后,我们需要去给整个界面去添加 键盘监听事件
,
当事件被触发时,需要执行 this
(即本类) 中的代码。
// 给整个界面添加键盘监听事件
this.addKeyListener(this);
现在我要在游戏中 上下左右移动
,既然是移动,那一定会跟这里的 0
发生关系。
向上移动
:将 0
下面的图片往上移动。
向下移动
:将 0
上面的图片往下移动。
不管你怎么移,你都得先知道 0
这个位置在哪才可以,因此我们需要先去统计一下 0
所在的位置。
这个应该在 initData()
(打乱数据) 方法中统计:在将一维数组的值添加到二维数组中时,就可以来做一个 if判断
。
如果你当前遍历到的位置 为0 的话,此时,就应该将 0
的位置记录下来。
记录 0
的位置我可以在方法前面定义两个变量 x 和 y
, x 和 y
就表示 0
在二维数组中的位置。
如果你不为 0
,我才把数字添加到二维数组中。
// 记录空白方块在二维数组中的位置
int x = 0;
int y = 0;
// 初始化数据(打乱)
private void initData() {
//5.给二维数组添加数据
//遍历一维数组tempArr得到每一个元素,把每一个元素依次添加到二维数组当中
for (int i = 0; i < tempArr.length; i++) {
if (tempArr[i] == 0) {
x = i / 4;
y = i % 4;
} else { // 如果你不为 `0`,我才把数字添加到二维数组中
data[i / 4][i % 4] = tempArr[i];
}
}
}
PS:建议将成员变量都定义在最上面
它是没有上下顺序的,没有规定谁一定要写在上面。只不过我们平时在写代码的时候,为了方便阅读,一般来讲都是把它们写在最上面的。
接着,找到 keyReleased()
重写方法,在这个方法中我们才能对 上、下、左、右
进行判断。
向左的按钮
对应的数字是 37
,向上的按钮
所对应的数字是 38
,向右的按钮
所对应的数字是 39
,向下的按钮
所对应的数字是 40
。
那这个是怎么知道的呢?难不成我要背吗? —— 不需要
在这只需要将 code值
打印出来就行了。
@Override
public void keyReleased(KeyEvent e) {
int code = e.getKeyCode();
System.out.println(code);
}
然后打开 App
右键运行,然后在整个界面中去按 上下左右
,就会出现如下数字。
因此这个数字不需要去记,以后要用了打印一下即可。
这些数字其实是有规律的:从左开始,左上右下分别是37、38、39、40。
接下来,针对于这四种情况,就要来写四个判断。
@Override
public void keyReleased(KeyEvent e) {
int code = e.getKeyCode();
if (code == 37) {
System.out.println("向左移动");
} else if (code == 38) {
System.out.println("向上移动");
} else if (code == 39) {
System.out.println("向右移动");
} else if (code == 40) {
System.out.println("向下移动");
}
}
然后打开 App
右键运行,然后在整个界面中去按 左上右下
,就会出现如下文字。
因此接下来我们只需要将判断里面的代码去改写一下就OK了。
首先我们先来写向上移动,因为向上移动它比较好理解。
三、向上移动
向上移动实际的业务逻辑应该是:把空白方块下方的数字网上移动。
我们已经知道了 x、y
就表示 空白方块
,那空白方块下面的,我该怎么表示呢? —— (x + 1, y)
因此在代码中,我们只需要将 (x, y)
和 (x + 1, y)
两个位置的数据来做一个交换就行了。
最后调用方法,按照最新的数字去加载图片即可。
@Override
public void keyReleased(KeyEvent e) {
int code = e.getKeyCode();
if (code == 37) {
System.out.println("向左移动");
} else if (code == 38) {
System.out.println("向上移动");
// 把空白方块下方的数字赋值给空白方块
// PS:这里不用使用到临时变量
// int tmp = data[x][y];
data[x][y] = data[x + 1][y];
// data[x + 1][y] = tmp;
data[x + 1][y] = 0;
x++;
// 调用方法按照最新的数字加载图片
initImage();
} else if (code == 39) {
System.out.println("向右移动");
} else if (code == 40) {
System.out.println("向下移动");
}
}
代码写完后,打开 App
右键运行,会发现,当你按了 向上的键
后,事件被触发了,但是游戏界面并没有动,这是为什么呢?
这就是我们最后的一个小细节:在 initImage()
方法中,我们还要做一个操作。
在一开始,我们应该清楚原本已经出现的所有图片,并且在方法的最后,需要去刷新一下界面。
// 初始化图片
private void initImage() {
// 情况原本已经出现的所有图片
this.getContentPane().removeAll();
// 之前的代码
...............
// 刷新一下界面
this.getContentPane().repaint();
}
代码写完后,打开 App
右键运行,然后按住 上键
,可以发现图片已经上下移动了。
四、将 下、左、右
三个方向做一个代码实现
向下移动
的逻辑:跟向上移动的逻辑是反过来的,它其实就是把空白方块上方的图片往下移动。
向左移动
的逻辑:将 0
右边的图片向左移动。
向右移动
的逻辑:将 0
左边的图片向右移动。
//松开按键的时候会调用这个方法
@Override
public void keyReleased(KeyEvent e) {
//对上,下,左,右进行判断
//左:37 上:38 右:39 下:40
int code = e.getKeyCode();
System.out.println(code);
if (code == 37) {
System.out.println("向左移动");
//逻辑:
//把空白方块右方的数字往左移动
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++;
//调用方法按照最新的数字加载图片
initImage();
} else if (code == 38) {
System.out.println("向上移动");
//逻辑:
//把空白方块下方的数字赋值给空白方块
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++;
//调用方法按照最新的数字加载图片
initImage();
} else if (code == 39) {
System.out.println("向右移动");
//逻辑:
//把空白方块左方的数字往右移动
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--;
//调用方法按照最新的数字加载图片
initImage();
} else if (code == 40) {
System.out.println("向下移动");
//逻辑:
//把空白方块上方的数字往下移动
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--;
//调用方法按照最新的数字加载图片
initImage();
}
}
代码写完后,打开 App
右键运行,此时我们就可以完整的去玩一下这个游戏了。
五、优化代码
当 空白的格子
在最下面时,此时我再去按 上键
,程序虽然没有崩,但是在控制台就报异常了。
在 "AWT-EventQueue-0"
里面出现了一个叫 ArrayIndexOutOfBoundsException
:数组的索引越界异常。
这些异常我们该如何看呢?其实有一个小技巧,我们可以看前面的包名,从第三行开始的代码,包名都是 java.desktop/java.awt.Component.....
包下的,也就是说下面的这些代码不是我们写的,而是Java写的,Java那些大佬他们也有可能会发生错误,但是不会有这么低级的问题。因此肯定是我们写的代码出问题了。
来看第二行我们自己写的代码的包名:com.itheima.ui
包下的 GameJFrame
的 keyReleased()
方法中的 191行
出问题了。
Exception in thread "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4
at com.itheima.ui.GameJFrame.keyReleased(GameJFrame.java:191)
at java.desktop/java.awt.Component.processKeyEvent(Component.java:6587)
at java.desktop/java.awt.Component.processEvent(Component.java:6403)
at java.desktop/java.awt.Container.processEvent(Container.java:2266)
at java.desktop/java.awt.Window.processEvent(Window.java:2056)
at java.desktop/java.awt.Component.dispatchEventImpl(Component.java:5001)
at java.desktop/java.awt.Container.dispatchEventImpl(Container.java:2324)
at java.desktop/java.awt.Window.dispatchEventImpl(Window.java:2780)
at java.desktop/java.awt.Component.dispatchEvent(Component.java:4833)
点击一下出问题的蓝色字体,它就会自动跳到出问题的行。
你需要想:为什么会出问题呢?
假设现在的空白区域是最下方:(3, 0), (3, 1), (3, 2)
,如果在这种情况下,x
继续 + 1
时,数组就超出索引了。
因此在上面我们需要对它的边界情况做一些判断:如果超出索引了,就需要直接 return
。
以向上移动为例:if(x == 3) return
:表示空白方块已经在最下方了,他的下面没有图片再能移动了
//松开按键的时候会调用这个方法
@Override
public void keyReleased(KeyEvent e) {
//对上,下,左,右进行判断
//左:37 上:38 右:39 下:40
int code = e.getKeyCode();
System.out.println(code);
if (code == 37) {
System.out.println("向左移动");
if(y == 3){
return;
}
//逻辑:
//把空白方块右方的数字往左移动
data[x][y] = data[x][y + 1];
data[x][y + 1] = 0;
y++;
//调用方法按照最新的数字加载图片
initImage();
} else if (code == 38) {
System.out.println("向上移动");
if(x == 3){
//表示空白方块已经在最下方了,他的下面没有图片再能移动了
return;
}
//逻辑:
//把空白方块下方的数字往上移动
//x,y 表示空白方块
//x + 1, y 表示空白方块下方的数字
//把空白方块下方的数字赋值给空白方块
data[x][y] = data[x + 1][y];
data[x + 1][y] = 0;
x++;
//调用方法按照最新的数字加载图片
initImage();
} else if (code == 39) {
System.out.println("向右移动");
if(y == 0){
return;
}
//逻辑:
//把空白方块左方的数字往右移动
data[x][y] = data[x][y - 1];
data[x][y - 1] = 0;
y--;
//调用方法按照最新的数字加载图片
initImage();
} else if (code == 40) {
System.out.println("向下移动");
if(x == 0){
return;
}
//逻辑:
//把空白方块上方的数字往下移动
data[x][y] = data[x - 1][y];
data[x - 1][y] = 0;
x--;
//调用方法按照最新的数字加载图片
initImage();
}
}
代码写完后,打开 App
右键运行,一直按 向下
或者其他的方向键,在控制台也不会出现报错信息了。
六、总结
在刚刚,我们是这么做的
1、让本类实现 KeyListener接口
,并重写所有的抽象方法
2、给整个游戏界面添加键盘监听事件
因为我们是在游戏界面中按 上下左右键
去移动数字方块的。
3、在移动的时候,其实就是把空白对应的数字 0
跟它 上下左右
的数字进行一个交换
首先我们需要来统计一下空白方块对应的数字 0
在二维数组中的位置。
4、知道了位置后,就可以在 keyReleased()
重写方法中实现移动的逻辑
说到这里有同学就会想:keyReleased()
是按键抬起来才会触发的,那我能不能在 keyPressed()
(按下) 的方法中写呢?
其实也可以,但是如果你按下但不松开,它就会反复去调用 keyPressed()
方法,这不是我想要的,我需要的一个是:按一下就移一次,因此我们应该在 keyReleased()
(抬起来) 的方法中去实现 上下左右
移动的业务逻辑。
5、Bug修复:
- 当空白方块在最下面的时候,它是无法上移的
- 当空白方块在最上面的时候,它是无法下移的
- 当空白方块在最左面的时候,它是无法右移的
- 当空白方块在最右面的时候,它是无法左移的