Skip to content
LiuJiawei4132 edited this page Mar 30, 2019 · 5 revisions

我对这个小游戏的感悟

用了几天的时间,借鉴了下别人的代码做出来的2048,虽然大部分是借鉴人家的代码,但是写出来的时候成就感还是很强的, 并且也第一次接触到了MVC程序设计模式,我个人认为大概的方式就是:先设计好视图,再建立起基本的模型, 最后的再加入控制器调试。最关键的内容还是瓦片的合并算法以及类的设计。

效果如下:

需要使用到的类

  • javax.swing.
    • JFrame 窗口
    • JLable 标签
    • JPanel 面板
  • java.awt.event.
    • KeyEvent 键盘监听事件号
    • KeyListener 键盘监听
  • java.util.
    • ArrayList 根据下标遍历、访问元素效率较高,但增删效率较慢
    • List
    • Random 获取随机数

第一步完成的功能:写一个界面

  • 主函数
public class GameMain {
    public static void main(String[] args) {
        Window window = new Window();
        window.initView();
        window.setTitle("D2048"); // 设置标题
        window.getContentPane().setPreferredSize(new Dimension(400, 500)); //对JFrame添加组件的一种方式
        //JFrame直接调用setBackground设置背景色不生效
        window.getContentPane().setBackground(new Color(0xfaf8ef)); // 设置背景颜色
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);  // 退出程序
        window.setResizable(false); //去掉最大化按钮
        window.pack();    //获得最佳大小
        window.setVisible(true);    // 设置可见
    }
}
  • 接下来要思考需要用到的变量
class Window extends JFrame {
    private static int score = 0; // 分数
    final Font[] fonts = {new Font("", Font.BOLD, 48)
            , new Font("", Font.BOLD, 42)
            , new Font("", Font.BOLD, 36)
            , new Font("", Font.BOLD, 30)
            , new Font("", Font.BOLD, 24)
    };
    private GameBoard gameBoard;
    private JLabel ltitle;
    private JLabel lsctip;
    private JLabel lgatip;
    private JLabel lscore;
}
  • 配置并添加组件至Windows面板
public void initView() {
        ltitle = new JLabel("2048", JLabel.CENTER); // 建立一个JLabel标签
        ltitle.setFont(new Font("", Font.BOLD, 50)); // 设置字体
        ltitle.setForeground(new Color(0x776e65)); // 设置前景颜色
        ltitle.setBounds(0, 0, 120, 60); // 设置大小和位置
    
        lgatip = new JLabel("按方向键可以控制方块的移动,按ESC键可以重新开始游戏.", JLabel.CENTER);
        lgatip.setFont(new Font("", Font.ITALIC, 13));
        lgatip.setForeground(new Color(0x776e65));
        lgatip.setBounds(10, 75, 340, 15);
    
        lsctip = new JLabel("SCORE", JLabel.CENTER); // 建立一个JLabel标签
        lsctip.setFont(new Font("", Font.BOLD, 18)); // 设置字体
        lsctip.setForeground(new Color(0xeee4da)); // 设置前景颜色
        lsctip.setOpaque(true);                    // 设置透明度
        lsctip.setBackground(new Color(0xbbada0)); // 设置背景颜色
        lsctip.setBounds(290, 5, 100, 25);       // 设置背景大小以及位置
    
        lscore = new JLabel("0", JLabel.CENTER);
        lscore.setFont(new Font("", Font.BOLD, 25));
        lscore.setForeground(Color.WHITE);
        lscore.setOpaque(true);
        lscore.setBackground(new Color(0xbbada0));
        lscore.setBounds(290, 30, 100, 25);
    
        gameBoard = new GameBoard(); // 
        gameBoard.setPreferredSize(new Dimension(400, 400)); // 设置一个大小动态的窗口
        gameBoard.setBackground(new Color(0xbbada0));
        gameBoard.setBounds(0, 100, 400, 400); // 主要游戏面板为400×400
        gameBoard.setFocusable(true); // 设置控件为可获取焦点状态,只能设置成true才能获取事件
    
        this.add(ltitle);
        this.add(lsctip);
        this.add(lgatip);
        this.add(lscore);
        this.add(gameBoard);
}

效果如下:

  • 接下来就要考虑Tile的类该怎么写了 先想好Tile所具有的属性,例如每个Tile的数值、Tile是否已经合成了、Tile该如何初始化?
    class Tile {
        public int value; // 记录瓦片的值
        public boolean isMerge; // 判断是否为合成瓦片

        public Tile() {
            clearData();
        }

        public void clearData() { // 初始化瓦片
            this.value = 0;
            this.isMerge = false;
        }
    }

考虑到每个瓦片对应的数值不同,字体大小以及背景颜色都不同,所以我们要给写个获取颜色和大小的方法

        public Font getFont() { // 获取字体大小
            int index = value < 100 ? 1 : value < 1000 ? 2 : value < 10000 ? 3 : 4;
            return fonts[index];
        }
        public Color getForeground() {  // 设置字体
            switch (value) {
                case 0:
                    // 颜色与瓦块背景色相同,因为每个瓦片初始值都为0,所以这时的字体颜色应与背景颜色一样
                    return new Color(0xcdc1b4); 
                case 2:
                case 4:
                    return new Color(0x776e65);
                default:
                    return new Color(0xf9f6f2);
            }
        }

        public Color getBackground() {
            switch (value) {
                case 0:
                    return new Color(0xcdc1b4);
                case 2:
                    return new Color(0xeee4da);
                case 4:
                    return new Color(0xede0c8);
                case 8:
                    return new Color(0xf2b179);
                case 16:
                    return new Color(0xf59563);
                case 32:
                    return new Color(0xf67c5f);
                case 64:
                    return new Color(0xf65e3b);
                case 128:
                    return new Color(0xedcf72);
                case 256:
                    return new Color(0xedcc61);
                case 512:
                    return new Color(0xedc850);
                case 1024:
                    return new Color(0xedc53f);
                case 2048:
                    return new Color(0xedc22e);
                case 4096:
                    return new Color(0x65da92);
                case 8192:
                    return new Color(0x5abc65);
                case 16384:
                    return new Color(0x248c51);
                default:
                    return new Color(0x248c51);
            }
        }
  • 接下来我们就要给瓦片画个背景啦
    class GameBoard extends JPanel {
        private static final int GAP_TILE = 16; // 4×80 = 320 间隙大小为 (400 - 320)/5 = 16
        private static final int ARC_TILE = 16; //瓦片圆角弧度
        private static final int SIZE_TILE = 80;//瓦片的大小 

        @Override
        public void paint(Graphics g) { 
            super.paint(g); // 覆写paint方法一定要先执行super.paint(g);
            for (int i = 0; i < 4; i++) {
                for (int j = 0; j < 4; j++) {
                    drawTile(g, i, j); 绘制4×4瓦片
                }
            }
        }
        public void drawTile(Graphics gg, int i, int j) {
            Graphics2D g = (Graphics2D)gg; // 向下转型成更强的画笔
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON); // 为画笔加上抗锯齿特效
            g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                    RenderingHints.VALUE_STROKE_NORMALIZE); // 笔划规范化控制提示值——几何形状应当规范化
            Tile tile = tiles[i][j];
            // 瓦片背景色
            g.setColor(tile.getBackground()); 
            // 绘制圆角矩形
            //fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight);
            g.fillRoundRect(GAP_TILE + (GAP_TILE + SIZE_TILE) * j // 位置 = 间距 + (瓦片大小 + 间距 * 位置) 
                        , GAP_TILE + (GAP_TILE + SIZE_TILE) * i
                        , SIZE_TILE, SIZE_TILE, ARC_TILE, ARC_TILE);
            // 字体颜色
            g.setColor(tile.getForeground());
            Font font = tile.getFont();
            g.setFont(font);
            FontMetrics fms = getFontMetrics(font);
            String value = String.valueOf(tile.value); // 将int转化成String
            g.drawString(value, GAP_TILE + (GAP_TILE + SIZE_TILE) * j 
                    + (SIZE_TILE - fms.stringWidth(value)) / 2 // 数值横坐标位置 = 瓦片位置 + (瓦片大小 + 字体长度) / 2
                    ,GAP_TILE + (GAP_TILE + SIZE_TILE) * i
                    + (SIZE_TILE - fms.getAscent() - fms.getDescent()) / 2
                    + fms.getAscent());
        }
    }

效果如下

那么2048游戏的界面就画好啦,接下来就需要根据游戏规则实现功能啦

第二步完成的功能:实现游戏初始化

  1. 随机生成两个瓦片
  2. 瓦片数值为 2 or 4
    class GameBoard extends JPanel {
        private Tile[][] tiles = new Tile[4][4];
        private boolean isEnd;
        private boolean isMove;

        public void createTile() { // 生成一个瓦片
            List<Tile> tileList = getBlankTile(); // 获取数值为0的瓦片列表
            if (!tileList.isEmpty()) { // 如果不为空
                Random random = new Random();
                int index = random.nextInt(tileList.size()); // 随机选取列表中的一个瓦片
                tileList.get(index).value = random.nextInt(100) > 50 ? 2 : 4; // 为其赋值(2 or 4)
            }
        }

        public List<Tile> getBlankTile() { // 获取空瓦片
            List<Tile> list = new ArrayList<Tile>();
            for (int i = 0; i < 4; i++) {
                for (int j = 0; j < 4; j++) {
                    if (tiles[i][j].value == 0)
                        list.add(tiles[i][j]);
                }
            }
            return list;
        }

        public GameBoard() {
            initGame();
        }

        public void initGame() {
            for (int i = 0; i < 4; i++) {
                for (int j = 0; j < 4; j++) {
                    tiles[i][j] = new Tile();
                }
            }

            score = 0; // 初始化分数
            createTile(); // 随机生成瓦片
            createTile();

            lscore.setText("0"); // 分数为0
            isMove = false; // 初始化游戏状态
            isEnd = false;
        }
    }

效果如下

第三步完成的功能:实现游戏功能

  1. 实现瓦片移动、合并、生成
    class GameBoard extends JPanel implements KeyListener { // 在此基础上添加监听类
        public GameBoard() {
            initGame();
            addKeyListener(this); // 初始化添加键盘监听事件
        }

        @Override
        public void keyPressed(KeyEvent e) {
            switch (e.getKeyCode()) {
                case KeyEvent.VK_ESCAPE: // Esc按键初始化
                    initGame();
                    break;
                case KeyEvent.VK_UP: // 方向键up
                    moveUp(); // 移动方法
                    movedCreateTile(); // 移动后生成瓦片
                    isGameOver(); // 判断游戏是否结束
                    break;
                // 这里只举例 up键
            }
            repaint();
        }

        public boolean moveUp() {
            for (int i = 0; i < 4; i++) {
                for (int j = 1; j < 4; j++) {
                    // 判断当前瓦片数值不能为空,前一个瓦片不能是合并瓦片,不能是瓦片的边界
                    for (int k = j; k > 0 && tiles[k][i].value != 0 && !tiles[k-1][i].isMerge; k--) {
                        if (tiles[k-1][i].value == 0) {
                            doMove(tiles[k-1][i], tiles[k][i]);
                        } else if (tiles[k-1][i].value == tiles[k][i].value) {
                            doMerge(tiles[k-1][i], tiles[k][i]);
                            break;
                        }
                    }
                }
            }
            return isMove;
        }
        public void doMove(Tile fir, Tile sec) {
            fir.swap(sec);
            sec.clearData();
            isMove = true;
        }

        public void doMerge(Tile fir, Tile sec) {
            fir.value = fir.value << 1; 
            fir.isMerge = true;
            sec.clearData();
            score += fir.value;
            isMove = true;
        }
        public void movedCreateTile() { // 生成瓦片
            if (isMove) {
                createTile();
            ·    isMove = false;
            }
        }
        @Override
        public void keyTyped(KeyEvent e) {

        }

        @Override
        public void keyReleased(KeyEvent e) {

        }
  1. 判断游戏是否结束并打印提示语
        public void isGameOver() {
            lscore.setText("" + score);·

            if (!getBlankTile().isEmpty()) {
                return;
            }

            for (int i = 0; i < 4; i++) {
                for (int j = 3; j > 0; j--) {
                    if (tiles[j-1][i].value == tiles[j][i].value || tiles[i][j-1].value == tiles[i][j].value)
                        return;
                }
            }
            isEnd = true;
        }
        @Override
        public void paint(Graphics g) { // 添加
            // 在paint方法原有上添加功能
            if (isEnd) {
                g.setColor(new Color(255,255,255,180));
                g.fillRect(0, 0, getWidth(), getHeight());
                g.setColor(new Color(0x3d79ca));
                g.setFont(fonts[0]);
                FontMetrics fms = getFontMetrics(fonts[0]);
                String value = "Game over";
                String score = "SCORE:" + Window.score;
                g.drawString(value, (getWidth() - fms.stringWidth(value)) / 2 , getHeight() / 3);
                g.drawString(score + "", (getWidth() - fms.stringWidth(score)) / 2, getHeight() / 2 + getHeight() / 4);
            }
        }

效果如下
2048-5


总结

2048小游戏的代码虽然不多,但是要做到分配好功能和类也还是一件不容易的事情。每个类要有他特有的功能,这个类需要完成什么功能,哪些功能可以做成一个方法。例如getBlankTile()方法,返回数值为空的瓦片列表。我以前的想法一定是将他和createTile()方法合并起来,并不会考虑到哪里还会再次用到这个方法。
还有的就是列表的使用,在看完列表的功能后我也并没有想过到底又哪些运用到它的地方,那么生成随机瓦片的画风一定是随机在二维数组中抽一个瓦片,判断它数值是否为0,如果不是就继续判断下去,直到没有空的瓦片,现在想想我还是真的蠢哈哈哈哈。
而这里它的做法是,创建一个容器,将所有空的瓦片添加到列表中,要生成的瓦片就在这个容器里面抓,真是太棒了。
类似的功能还有很多很多,这些都是我写代码时不曾注意到的地方。

        public void createTile() {
            List<Tile> tileList = getBlankTile();
            if (!tileList.isEmpty()) {
                Random random = new Random();
                int index = random.nextInt(tileList.size());
                tileList.get(index).value = random.nextInt(100) > 50 ? 2 : 4;
            }
        }

        public List<Tile> getBlankTile() {
            List<Tile> list = new ArrayList<Tile>();
            for (int i = 0; i < 4; i++) {
                for (int j = 0; j < 4; j++) {
                    if (tiles[i][j].value == 0)
                        list.add(tiles[i][j]);
                }
            }
            return list;
        }
        public void isGameOver() {
            lscore.setText("" + score);

            if (!getBlankTile().isEmpty()) {
                return;
            }

            for (int i = 0; i < 4; i++) {
                for (int j = 3; j > 0; j--) {
                    if (tiles[j-1][i].value == tiles[j][i].value || tiles[i][j-1].value == tiles[i][j].value)
                        return;
                }
            }
            isEnd = true;
        }