摘要:实现的原理类似于做一个心脏跳动的动画,心脏跳动时,心脏渐渐地变大,到达一定的大小后,又渐渐地变小,直到恢复为原来的大小。
在《2048游戏Java版本+源码(一)》文章中详细讲解了《2048》游戏基本功能的实现过程,思路会比较清晰,实现的代码也是比较简洁的。如果希望了解源码的实现,建议先看一下《2048游戏Java版本+源码(一)》,相信看源码会更加容易。
上一版本的《2048》游戏中只是实现了基本功能,没有音效,没有特效,总感觉游戏缺少一些元素。为了打造一个更加完美的游戏,在这一版本的实现中会加入音效和特效,源码的结构相应地会做一些修改,逻辑看起来可能会没有前一版本的清晰,因为这是优化后的结果了,用了一些技巧。


图1《2048》效果图


加入音效

对于游戏,我打算在移动时加入移动的音效,在合并的时候加入合并的音效,首先,我们应该先准备两个音效文件,分别为:
move.wav
merge.wav
因为Swing的GUI处理是单线程的,在主线程播放音效有可能会阻塞界面的响应,所以我们应该把播放音效的代码放在另外的一个线程中。
那么什么时候调用代码播放音效呢?很明显,我们是通过监听方向键的点击事件来处理瓦片的移动和合并过程的,所以应该在方向键点击事件中加入播放的音效。为了更清晰展示修改的过程,对比一下两个版本监听器的实现,代码如下:
//版本一
public void keyPressed(KeyEvent e) {
    boolean moved = false;
    switch (e.getKeyCode()) {
        ...
        case KeyEvent.VK_LEFT:
            moved = moveLeft();
            inovkeCreateTile();
            checkGameOver(moved);
            break;
        ...
}
//版本二
public void keyPressed(KeyEvent e) {
    boolean moved = false;
    switch (e.getKeyCode()) {
        ...
        case KeyEvent.VK_LEFT:
            moved = moveLeft();
            invokeAnimate();
            checkGameOver(moved);
            break;
        ...
}

版本二只是更换了一个invokeAnimate()方法,这个方法会实现音效的播放和特效过程,代码如下:
private void invokeAnimate() {
    if (isMerge) {
        new WaveThread("merge.wav").start();
        moveAnimate();
        mergeAnimate();
    } else if (isMove) {
        new WaveThread("move.wav").start();
        moveAnimate();
    }
    if (isMerge || isMove) {
        createTile();
        isMerge = false;
        isMove = false;
    }
}

可以清楚地看到,如果在上一操作有对瓦片做合并,则播放merge.wav合并音效,如果有对瓦片做移动,则播放move.wav移动音效。
在此处还可以做一下优化,因为每次操作播放音效都会创建一个线程,这样会浪费很多的资源,可以考虑使用线程池,由于操作不会太频繁,创建一个固定大小的线程池就好了。

更多详细的线程池知识可以参考:《Java线程池详解》;
详细的播放音效的线程代码可以参考:《如何在Swing GUI中加入音效》。


加入移动特效

看完上面的描述,加入音效还是很简单的,代码的改动也不多。但是加入移动特效,需要修改的代码可能就比较多了,看一下moveAnimate()方法,代码如下:
private void moveAnimate() {
    isAnimate = false;
    Graphics gg = getGraphics();
    Image image = this.createImage(getWidth(), getHeight());
    Graphics g = image.getGraphics();
    g.setColor(bgColor);
    g.fillRect(0, 0, getWidth(), getHeight());
    int k = 0; //
    while (k < PAINT_NUM) {
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                int step = (GAP_TILE + SIZE_TILE) * tiles[i][j].step / PAINT_NUM;
                switch (tiles[i][j].directEnum) {
                    case LEFT:
                        drawTile(g, i, j, (PAINT_NUM - k) * step, 0, 0);
                        break;
                    case RIGHT:
                        drawTile(g, i, j, (k - PAINT_NUM) * step, 0, 0);
                        break;
                    case UP:
                        drawTile(g, i, j, 0, (PAINT_NUM - k) * step, 0);
                        break;
                    case DOWN:
                        drawTile(g, i, j, 0, (k - PAINT_NUM) * step, 0);
                        break;
                    case NONE:
                        drawTile(g, i, j, 0, 0, 0);
                        break;
                }
            }
        }
        gg.drawImage(image, 0, 0, null);
        k++;
    }
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            tiles[i][j].step = 0;
            tiles[i][j].directEnum = DirectEnum.NONE;
        }
    }
    isAnimate = true;
}
先获取面板Graphics对象,然后创建一个图片对象Image,在Image上绘制背景和瓦片,最后把这个Image渲染到面板上。
移动特效的关键过程在于把瓦片移动的起始位置到结束位置的过程绘制出来,像放电影那样,把动作的所有帧在一定时间内移动起来就形成了动作。
首先计算瓦片起始位置与结束位置的长度,然后把长度分割到PAINT_NUM块,代码如下:
int step = (GAP_TILE + SIZE_TILE) * tiles[i][j].step / PAINT_NUM;
代码中使用while循环对k变量做循环判断,当k小于常量PAINT_NUM 时,执行drawTile()方法绘制所有的瓦片,对于有做了移动的瓦片,每次循环都改变原来的位置,改变的大小为step。这样就会把移动的瓦片由起始位置到结束位置的每一帧绘制出来,由于绘制的整个过程时间很短,就会形成一种移动的特效了。

为了计算起始位置到结束位置的长度,我们需要知道瓦片移动的次数,而且需要知道瓦片移动的方向,所以需要修改Tile的属性,加入两个属性,代码如下:
public int step; //移动的步数
public DirectEnum directEnum;//移动的方向

移动的方向directEnum是一个枚举类型,定义了五个值,代码如下:
NONE,LEFT,RIGHT,UP,DOWN;

其中NONE表示瓦片没有移动,或者表示一种最终的状态。


加入合并的特效

实现的原理类似于做一个心脏跳动的动画,心脏跳动时,心脏渐渐地变大,到达一定的大小后,又渐渐地变小,直到恢复为原来的大小。来看一下mergeAnimate()方法的实现,代码如下:
private void mergeAnimate() {
    isAnimate = false;
    Graphics gg = getGraphics();
    Image image = this.createImage(getWidth(), getHeight());
    Graphics g = image.getGraphics();
    g.setColor(bgColor);
    g.fillRect(0, 0, getWidth(), getHeight());
    int k = -3; //使下面的ex从0开始
    while (k < 4) {
        int ex = 9 - k ^ 2; //抛物线公式
        for (int i = 0; i < 4; i++) {
            for (int j = 0; j < 4; j++) {
                if (!tiles[i][j].ismerge) {
                    drawTile(g, i, j, 0, 0, 0);
                } else {
                    drawTile(g, i, j, 0, 0, ex);
                }
            }
        }
        gg.drawImage(image, 0, 0, null);
        k++;
    }
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            tiles[i][j].ismerge = false;
        }
    }
    isAnimate = true;
}

对于合并的Tile,即属性ismerge=true,绘制时加入了一个参数ex,根据心跳动画的原理,参数ex的值应该是渐大然后渐小,对应的数学模型是一个抛物线(数学知识还记得吧,哈哈),由于整个渐大渐小的过程是对称的,所以我使用了一个中轴为0的抛物线,代码如下:
int ex = 9 - k ^ 2; //抛物线公式

只要k的值保持递增,那么计算得到的ex值先是递增,达到9后,然后递减。


总结

两篇文章看完后,你是否可以自己也实现一个《G2048》游戏了呢?其实编写每一个游戏的关键都在于设计地图和设计元素的属性,需要抽象界面元素的属性用代码表示和用数据结构来存储地图的元素。
《G2048》这个游戏的地图很简单,用一个二维数组就可以存储了,关键在于对瓦片属性的抽象,当然作者一开始也陷入错误的思路,瓦片属性没有抽象好,陷入一些错误陷阱,现在看到的Tile是经过慢慢的优化后的结果。所以当你想实现的程序时,也没必要纠结这个方案到底可不可行,先用最简单的思路实现出来,然后再对代码做优化,最终你得到的程序会是一个比较完美的。


游戏的源码已经放在github上托管,有兴趣的童鞋可以下载玩玩,地址:
https://github.com/beyondfengyu/G2048

如果你希望跟作者讨论或者有交流技术,可以加群:399643539


版权说明:如无特殊说明,文章均为本站原创,如需转载请注明出处

本文标题:《2048》游戏Java版本+源码(二)

本文地址:http://www.wolfbe.com/detail/201609/369.html

本文标签: 游戏 2048 java

感谢您的支持,朗度云将继续前行

扫码打赏,金额随意

温馨提醒:打赏一旦完成,金额无法退还,请谨慎操作!

扫二维码 我要反馈 回到顶部