JAVA实现2048小游戏

2048小游戏也算是一款好玩的益智休闲小游戏,下面本博主用 java 语言将该游戏复现,感兴趣的小伙伴点击 关注 哦!

同时博主还用 python 语言复现了该游戏,可点击以下链接浏览博主的另一篇文章:Python实现2048小游戏 


目录

一、效果

二、教程

三、代码


一、效果

2048小游戏是一款比较流行的数字游戏,游戏规则如下:

每次可以选择上下左右其中一个方向去滑动,每滑动一次,所有的数字方块都会往滑动的方向靠拢外,系统也会在空白的地方乱数出现一个数字方块,相同数字的方块在靠拢、相撞时会相加。不断的叠加最终拼凑出2048这个数字就算成功。

                     

ps: 博主就没有添加成功的图片了,实在是因为技术不行,试完了几次均没有凑成 2048 ...

二、教程

1、使用IDEA搭建一个项目,项目名称:Game2048_java(可根据自己的喜好)

扫描二维码关注公众号,回复: 12471869 查看本文章

具体搭建过程可看博文用IDEA构建一个简单的Java程序范例,这里就不详细说了。

2、Data.interface

(1)导入包

import java.awt.Font;

(2)接口的定义

public interface Data {}

(3)数据类型的定义

  • title: Font型,窗口的标题字体类型和大小,即为 ‘2048’;
  • score: Font型,分数的字体类型和大小,即为‘得分’;
  • tips: Font型,说明文字的字体类型和大小,即为‘操作: ↑ ↓ ← →, 按esc键重新开始’;
  • font1: 表格中数字 2、4、8字体类型和大小
  • font2: 表格中数字 16、32、64字体类型和大小
  • font3: 表格中数字128、256、 512字体类型和大小
  • font4: 表格中数字1024、2048字体类型和大小
  • CHART_GAP: int型,表格与表格之前的空隙距离;
  • CHART_ARC: int型,表格的弧度值;
  • CHART_SIZE: int型,表格的大小。
Font title = new Font("微软雅黑", Font.BOLD, 50); 
Font score = new Font("微软雅黑", Font.BOLD, 28); 
Font tips = new Font("宋体", Font.PLAIN, 20); 

Font font1 = new Font("宋体", Font.BOLD, 46);
Font font2 = new Font("宋体", Font.BOLD, 40);
Font font3 = new Font("宋体", Font.BOLD, 34);
Font font4 = new Font("宋体", Font.BOLD, 28);

int CHART_GAP = 10;
int CHART_ARC = 20;
int CHART_SIZE = 86;

3、Form.class

(1)导入包

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.event.KeyListener;

(2)接口的实现

public class Form implements Data{}

(3)构造函数 —— 游戏窗体的搭建

A.窗口设置

JFrame frame = new JFrame("Game 2048");
frame.setSize(400, 530); // width * height
frame.setResizable(false); // 窗口大小不可调整
frame.setVisible(true); // true窗口可见
frame.setLocationRelativeTo(null); // 窗口居中
frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE); // 窗口的关闭
frame.setLayout(null); // 设置用户界面上的屏幕组件的格式布局,默认为流式布局

B.添加标签

  • 添加标题:2048  
JLabel ltitle = new JLabel("2048", JLabel.CENTER);
frame.add(ltitle);
ltitle.setFont(Data.title); // 标题字体/大小/位置
ltitle.setForeground(Color.BLACK);
ltitle.setBounds(50, 0, 150, 60);
  • 添加分数:得分:0
JLabel lscorename = new JLabel("得 分", JLabel.CENTER);
frame.add(lscorename);
lscorename.setFont(Data.score); // "得分"字体/大小/位置
lscorename.setForeground(Color.WHITE);
lscorename.setOpaque(true); // 设置控件是否透明.true: 控件不透明; false: 控件透明.
lscorename.setBackground(Color.GRAY);
lscorename.setBounds(250, 0, 120, 30);

lscore = new JLabel("0", JLabel.CENTER);
frame.add(lscore);
lscore.setFont(Data.score); // "得分"字体/大小/位置
lscore.setForeground(Color.WHITE);
lscore.setOpaque(true); // 设置控件是否透明.true: 控件不透明; false: 控件透明.
lscore.setBackground(Color.GRAY);
lscore.setBounds(250, 30, 120, 30);
  • 游戏说明
JLabel ltips = new JLabel("操作: ↑ ↓ ← →, 按esc键重新开始  ", JLabel.CENTER);
frame.add(ltips);
ltips.setFont(Data.tips); // "说明"字体/大小/位置
ltips.setForeground(Color.DARK_GRAY);
ltips.setBounds(0, 60, 400, 40);

C.游戏面板

JPanel panel = new Game2048Panel();
frame.add(panel);
panel.setBounds(0, 100, 400, 400); // 面板绘制区域
panel.setBackground(Color.GRAY);
panel.setFocusable(true); //setFocusable设置组件是否可被选中
// FlowLayout(流式布局): 组件按照加入的先后顺序按照设置的对齐方式从左向右排列,一行排满到下一行开始继续排列
panel.setLayout(new FlowLayout());
frame.addKeyListener((KeyListener) panel);  // 键盘监听

(4)主函数

public static void main(String[] args) {
        new Form();
    }

4、Chart.class —— 表格

(1)导入包

import java.awt.Color;
import java.awt.Font;

(2)接口的实现

public class Chart implements Data{}

(3) 数据类型的定义

  • value: int型,表格中的数值

(4)基本方法

A. 构造函数

 public Chart(){
        clear();
    }

B. 清除面板

public void clear(){
        value = 0;
    }

C. 设置数字字体颜色

对应数字的字体颜色

  • 0:  0xcdc1b4, 同背景颜色相同,因此不会显示
  • 2, 4:  BLACK, 黑色
  • 其他: WHITE, 白色
public Color getForeground(){
        return switch (value) {
            case 0 -> new Color(0xcdc1b4);
            case 2, 4 -> Color.BLACK;
            default -> Color.WHITE;
        };
    }

D. 设置数字背景颜色

public Color getBackground(){
        return switch (value){
            case 0 -> new Color(0xcdc1b4);
            case 2 -> new Color(0xeee4da);
            case 4 -> new Color(0xede0c8);
            case 8 -> new Color(0xf2b179);
            case 16 -> new Color(0xf59563);
            case 32 -> new Color(0xf67c5f);
            case 64 -> new Color(0xf65e3b);
            case 128 -> new Color(0xedcf72);
            case 256 -> new Color(0xedcc61);
            case 512 -> new Color(0xedc850);
            case 1024 -> new Color(0xedc53f);
            case 2048 -> new Color(0xedc22e);
            default -> new Color(0x248c51);
        };
    }

E. 设置数字字体大小

public Font getChartFont(){
        return switch (value){
            case 0, 2, 4, 8 -> font1;
            case 16, 32, 64 -> font2;
            case 128, 256, 512 -> font3;
            default -> font4;
        };
    }

5、Game2048Panel —— 面板

(1)导入包

import javax.swing.JPanel;

import java.awt.Graphics;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.FontMetrics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

(2)类的继承,接口的实现

  • JPanel类:面板组件,非顶层容器
  • KeyListener:键盘监听接口
public class Game2048Panel extends JPanel implements Data, KeyListener {}

(3)数据类型的定义

  • scores: int 型,记录游戏得分
  • charts: Chart型,方格数组
  • isadd: boolean型,是否新增数字

(4)构造方法

 public Game2048Panel() {
        initGame();  // 初始化
    }

(5)按键函数KeyPressed()

  • getKeyCode():键盘上每一个按钮都有对应码(Code),可用来查知用户按了什么键,返回当前按钮的数值
  • getKeyChar():处理的是比较高层的事件,返回的是每欠敲击键盘后得到的字符(中文输入法下就是汉字)
  • getKeyText():返回与此事件中的键关联的字符。比如getKeyText(e.getKeyCode())就返回你所按下的键盘
  • VK_ESCAPE: Esc
  • VK_UP: ↑
  • VK_DOWN: ↓
  • VK_LEFT: ←
  • VK_RIGHT: →
  • paint()方法用来绘制图形
  • repaint()方法用来重新绘制图像
public void keyPressed(KeyEvent e) {
    switch (e.getKeyCode()){
        // 重新开始游戏
        case KeyEvent.VK_ESCAPE:
            initGame(); // 重新开始游戏,游戏初始化
            break;
        // ↑
        case KeyEvent.VK_UP:
            MoveUp();  // 向左移动
            creatChart();  // 随机生成数字
            break;
        // ↓
        case KeyEvent.VK_DOWN:
            MoveDown();  // 向右移动
            creatChart();  // 随机生成数字
            break;
        // ←
        case KeyEvent.VK_LEFT:
            MoveLeft();  // 向左移动
            creatChart();  // 随机生成数字
            break;
        // →
        case KeyEvent.VK_RIGHT:
            MoveRight();  // 向右移动
            creatChart();  // 随机生成数字
            break;
        // others
        default:
            break;
    }
    repaint();
}

(6)游戏初始化

private void initGame(){
    scores = 0;
    for (int row = 0; row < 4; row++) {
        for (int col = 0; col < 4; col++) {
            charts[row][col] = new Chart();  // from Chart.java
        }
    }
    // 随机生成两个数
    for (int i = 0; i < 2; i++){
        isadd = true;
        creatChart();  // 随机生成一个数字
    }
}

(7)随机生成一个数字

2, 4出现概率3:1

  • random.nextInt(int n) 是参数 [0, n) 的随机数
  • random.nextInt(4): 随机生成 0, 1, 2, 3; 
private void creatChart(){
    List<Chart> list = getEmptyCharts();  // list 为空白方格

    if (!list.isEmpty() && isadd) {  // 在空白方格出随机生成一个数字
        Random random = new Random();
        int index = random.nextInt(list.size());
        Chart chart = list.get(index);
        chart.value = (random.nextInt(4) % 3 == 0) ? 2 : 4;
        System.out.println(chart.value);
        isadd = false;
    }
}

(8)获取空白方格

private List<Chart> getEmptyCharts(){
    List<Chart> chartList = new ArrayList<>();
    for (int i = 0; i < 4; i++){
        for (int j = 0; j < 4; j++){
            if (charts[i][j].value == 0){
                chartList.add(charts[i][j]);  //添加元素到 ArrayList 可以使用 add() 方法
            }
        }
    }
    return chartList;
}

(9)移动函数

A. 向上移动

private boolean MoveUp() {
    /* 向上移动,只需考虑第二行到第四行
       共分为两种情况:
       1、当前数字上边无空格,即上边值不为 0
          a. 当前数字与上边数字相等,合并
          b. 当前数字与上边数字不相等,continue
       2、当前数字上边有空格,即上边值为 0, 上移 */
    for (int j = 0; j < 4; j++) {
        for (int i = 1, index = 0; i < 4; i++) {
            if (charts[i][j].value > 0) {
                if (charts[i][j].value == charts[index][j].value) {
                    // 当前数字 == 上边数字
                /* 分数: 当前数字 + 上边数字
                   数值: 上边数字 = 上边数字 + 当前数字, 当前数字 = 0 */
                    scores += charts[i][j].value + charts[index][j].value;
                    charts[index][j].value = charts[i][j].value + charts[index][j].value;
                    charts[i][j].value = 0;
                    index += 1;
                    isadd = true;
                }
                // 当前数字与上边数字不相等,continue 可以省略不写
                else if (charts[index][j].value == 0) {
                    // 当前数字上边有0
                /* 分数: 不变
                   数值: 上边数字 = 当前数字, 当前数字 = 0 */
                   charts[index][j].value = charts[i][j].value;
                   charts[i][j].value = 0;
                   isadd = true;
                }
                else if (charts[++index][j].value == 0) {
                   // index 相当于慢指针,j 相当于快指针
                   // 也就是说快指针和慢指针中间可能存在一个以上的空格,或者index和j并未相邻
                   // 上边数字 = 0
                /* 分数: 不变
                   数值: 上边数字 = 当前数字, 当前数字 = 0 */
                   charts[index][j].value = charts[i][j].value;
                   charts[i][j].value = 0;
                   isadd = true;
                }
            }
        }
    }
    return isadd;
}

B. 向下移动

private boolean MoveDown(){
    /* 向下移动,只需考虑第一列到第三列
       共分为两种情况:
       1、当前数字下边无空格,即下边值不为 0
          a. 当前数字与下边数字相等,合并
          b. 当前数字与下边数字不相等,continue
       2、当前数字下边有空格,即下边值为 0, 下移 */
    for (int j = 0; j < 4; j++) {
        for (int i = 2, index = 3; i >= 0; i--) {
            if (charts[i][j].value > 0) {
                if (charts[i][j].value == charts[index][j].value) {
                    // 当前数字 == 下边数字
                    /* 分数: 当前数字 + 下边数字
                       数值: 下边数字 = 下边数字 + 当前数字, 当前数字 = 0 */
                    scores += charts[i][j].value + charts[index][j].value;
                    charts[index][j].value = charts[i][j].value + charts[index][j].value;
                    charts[i][j].value = 0;
                    index -= 1;
                    isadd = true;
                }
                // 当前数字与下边数字不相等,continue 可以省略不写
                else if (charts[index][j].value == 0) {
                    // 当前数字下边有0
                    /* 分数: 不变
                       数值: 下边数字 = 当前数字, 当前数字 = 0 */
                    charts[index][j].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
                else if (charts[--index][j].value == 0) {
                    // index 相当于慢指针,j 相当于快指针
                    // 也就是说快指针和慢指针中间可能存在一个以上的空格,或者index和j并未相邻
                    // 下边数字 = 0
                    /* 分数: 不变
                       数值: 下边数字 = 当前数字, 当前数字 = 0 */
                    charts[index][j].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
            }
        }
    }
    return isadd;
}

C. 向左移动

private boolean MoveLeft(){
    /* 向左移动,只需考虑第二列到第四列
       共分为两种情况:
       1、当前数字左边无空格,即左边值不为 0
          a. 当前数字与左边数字相等,合并
          b. 当前数字与左边数字不相等,continue
       2、当前数字左边有空格,即左边值为 0, 左移 */
    for (int i = 0; i < 4; i++) {
        for (int j = 1, index = 0; j < 4; j++) {
            if (charts[i][j].value > 0) {
                if (charts[i][j].value == charts[i][index].value) {
                    // 当前数字 == 左边数字
                    /* 分数: 当前数字 + 左边数字
                       数值: 左边数字 = 左边数字 + 当前数字, 当前数字 = 0 */
                    scores += charts[i][j].value + charts[i][index].value;
                    charts[i][index].value = charts[i][index].value + charts[i][j].value;
                    charts[i][j].value = 0;
                    index += 1;
                    isadd = true;
                }
                // 当前数字与左边数字不相等,continue 可以省略不写
                else if (charts[i][index].value == 0) {
                    // 当前数字左边有0
                    /* 分数: 不变
                       数值: 左边数字 = 当前数字, 当前数字 = 0 */
                    charts[i][index].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
                else if (charts[i][++index].value == 0) {
                    // index 相当于慢指针,j 相当于快指针
                    // 也就是说快指针和慢指针中间可能存在一个以上的空格,或者index和j并未相邻
                    // 左边数字 = 0
                    /* 分数: 不变
                       数值: 左边数字 = 当前数字, 当前数字 = 0 */
                    charts[i][index].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
            }
        }
    }
    return isadd;
}

D. 向右移动

private boolean MoveRight(){
  /* 向右移动,只需考虑第一列到第三列
     共分为两种情况:
     1、当前数字右边无空格,即右边值不为 0
        a. 当前数字与右边数字相等,合并
        b. 当前数字与右边数字不相等,continue
     2、当前数字右边有空格,即右边值为 0, 右移 */
    for (int i = 0; i < 4; i++) {
        for (int j = 2, index = 3; j >= 0; j--) {
            if (charts[i][j].value > 0) {
                if (charts[i][j].value == charts[i][index].value) {
                    // 当前数字 == 右边数字
                    /* 分数: 当前数字 + 右边数字
                       数值: 右边数字 = 右边数字 + 当前数字, 当前数字 = 0 */
                    scores += charts[i][j].value + charts[i][index].value;
                    charts[i][index].value = charts[i][j].value + charts[i][index].value;
                    charts[i][j].value = 0;
                    index -= 1;
                    isadd = true;
                }
                // 当前数字与左边数字不相等,continue 可以省略不写
                else if (charts[i][index].value == 0) {
                    // 当前数字右边有0
                    /* 分数: 不变
                       数值: 右边数字 = 当前数字, 当前数字 = 0 */
                    charts[i][index].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
                else if (charts[i][--index].value == 0) {
                    // index 相当于慢指针,j 相当于快指针
                    // 也就是说快指针和慢指针中间可能存在一个以上的空格,或者index和j并未相邻
                    // 右边数字 = 0
                    /* 分数: 不变
                       数值: 右边数字 = 当前数字, 当前数字 = 0 */
                    charts[i][index].value = charts[i][j].value;
                    charts[i][j].value = 0;
                    isadd = true;
                }
            }
        }
    }
    return isadd;
}

(10)判断游戏是否结束

private boolean judgeGameOver(){
    // 将lscore标签内容设置为 scores + ""
    Form.lscore.setText(scores + "");

    // 当空白空格不为空时,即游戏未结束
    if (!getEmptyCharts().isEmpty()){
        return false;
    }

    // 当空白方格为空时,判断是否存在可合并的方格
    for (int i = 0; i < 3; i++){
        for (int j = 0; j < 3; j++){
            if (charts[i][j].value == charts[i][j + 1].value
                || charts[i][j].value == charts[i + 1][j].value){
                return false;
            }
        }
    }
    // 若不满足以上两种情况,则游戏结束
    return true;
}

(11)判断游戏是否成功

private boolean judgeGameSuccess() {
    // 检查是否有2048
    for (int i = 0; i< 4; i++) {
        for (int j = 0; j < 4; j++) {
            if (charts[i][j].value == 2048) {
                return true;
            }
        }
    }
    return false;
}

(12)画笔函数

public void paint(Graphics g){
    super.paint(g);
    for (int i = 0; i < 4; i++){
        for (int j = 0; j < 4; j++){
            drawChart(g, i, j);
        }
    }

    // 如果游戏结束
    if (judgeGameOver()){
        g.setColor(new Color(64, 64, 64, 150));
        g.fillRect(0, 0, getWidth(), getHeight());  // 画矩形
        g.setColor(Color.WHITE);  // 画笔颜色为白色
        g.setFont(title);
        FontMetrics fm = getFontMetrics(title);
        String value = "Game Over!";  // 内容: Game Over!
        g.drawString(value,
                (getWidth() - fm.stringWidth(value)) / 2,
                getHeight() / 2);
    }  // 位置

    // 如果游戏成功
    if (judgeGameSuccess()) {
        g.setColor(new Color(64, 64, 64, 150));
        g.fillRect(0, 0, getWidth(), getHeight());  // 画矩形
        g.setColor(Color.RED);  // 画笔颜色为红色
        g.setFont(title);
        FontMetrics fm = getFontMetrics(title);
        String value = "Successful!";  // 内容: Successful!
        g.drawString(value,
                (getWidth() - fm.stringWidth(value)) / 2,
                getHeight() / 2);
      // 位置
    }
}

(13)绘制方格

A. Java语言在Graphics类提供绘制各种基本的几何图形的基础上, 扩展Graphics类提供一个Graphics2D类, 它拥用更强大的二维图形处理能力,提供、坐标转换、颜色管理以及文字布局等更精确的控制。

B. setRenderingHint() 方法的参数是一个键以及对应的键值。

    a. KEY_ANTIALIASING: 抗锯齿提示键。

    对象的几何形状呈现方法是否将尝试沿形状的边缘减少锯齿现象 此提示允许的值有:

  •         -- VALUE_ANTIALIAS_ON:使用抗锯齿模式完成呈现
  •         -- VALUE_ANTIALIAS_OFF:在不使用抗锯齿模式的情况下完成呈现
  •         -- VALUE_ANTIALIAS_DEFAULT:使用由实现选择的默认抗锯齿模式完成呈现

    b. KEY_STROKE_CONTROL: 笔划规范化控制提示键。

    c. STROKE_CONTROL 提示键控制呈现实现是否应该或允许出于各种目的而修改所呈现轮廓的几何形状。

        此提示允许的值有:

  •        -- VALUE_STROKE_NORMALIZE:几何形状应当规范化,以提高均匀性或直线间隔和整体美观。
  •        -- VALUE_STROKE_PURE:几何形状应该保持不变并使用子像素精确度呈现
  •        -- VALUE_STROKE_DEFAULT:根据给定实现的权衡,可以修改几何形状或保留原来的几何形状。

C. 绘制圆角

  • -- x: 填充矩形的 x 坐标
  • -- y: 填充矩形的 y 坐标
  • -- width: 填充矩形的宽度
  • -- height: 填充矩形的高度
  • -- arcwidth: 4个弧度的水平直径
  • -- archeight: 4个弧度的垂直直径

D. FontMetrics 字体属性类

  • GetAscent(): Ascent表示字体从基线到顶端的距离
  • getDescent(): Descent表示字体从基线到下降字符底端的距离
  • getLeading(): Leading 表示本文行之间的距离
  • getheight(): 字体高度 Ascent + Descent + Leading
  • StringWidth(String): 字符串宽度
private void drawChart(Graphics g, int i, int j){
    Graphics2D g2d = (Graphics2D) g;
    
    // 消除锯齿
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
    // 几何形状规范化
    g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
            RenderingHints.VALUE_STROKE_NORMALIZE);

    Chart chart = charts[i][j];
    g2d.setColor(chart.getBackground());  // 表格背景颜色
    g2d.fillRoundRect(CHART_GAP + (CHART_GAP + CHART_SIZE) * j,
            CHART_GAP + (CHART_GAP + CHART_SIZE) * i,
            CHART_SIZE, CHART_SIZE, CHART_ARC, CHART_ARC);
    g2d.setColor(chart.getForeground());  // 表格前景颜色
    g2d.setFont(chart.getChartFont());   // 设置字体

    // 文字设定
    FontMetrics fm = getFontMetrics(chart.getChartFont());
    String value = String.valueOf(chart.value);  // int型转换为String字符串
    g2d.drawString(value,
            CHART_GAP + (CHART_GAP + CHART_SIZE) * j +
                    (CHART_SIZE - fm.stringWidth(value)) / 2,
            CHART_GAP + (CHART_GAP + CHART_SIZE) * i +
                    (CHART_SIZE - fm.getAscent() - fm.getDescent()) / 2
                    + fm.getAscent());

}

三、代码

1、Data.interface

package Game2048_java;

import java.awt.Font;

public interface Data {
    Font title = new Font("微软雅黑", Font.BOLD, 50); // 窗口标题
    Font score = new Font("微软雅黑", Font.BOLD, 28); // 分数
    Font tips = new Font("宋体", Font.PLAIN, 20); // 说明

    /* font1: 数字2, 4, 8
    font2: 数字16, 32, 64
    font3: 数字128, 256, 512
    font4: 数字1024, 2048, 4096, 8192
    * */
    Font font1 = new Font("宋体", Font.BOLD, 46);
    Font font2 = new Font("宋体", Font.BOLD, 40);
    Font font3 = new Font("宋体", Font.BOLD, 34);
    Font font4 = new Font("宋体", Font.BOLD, 28);

    int CHART_GAP = 10;
    int CHART_ARC = 20;
    int CHART_SIZE = 86;
}

2、Form.class

package Game2048_java;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.event.KeyListener;

public class Form implements Data{
    public static JLabel lscore;

    /** 构造窗体 */
    public Form() {
        /*
        setSize(x, y): 窗口大小 x * y
        setVisible(true): true窗口可见
        setLocationRelativeTo((Component)null)设置窗口相对于指定组件的位置,null表示窗口在屏幕中央
        点击窗口右上角关闭,四种关闭方式:
        DO_NOTHING_ON_CLOSE,不执行任何操作。
        HIDE_ON_CLOSE,只隐藏界面,setVisible(false)。
        DISPOSE_ON_CLOSE,隐藏并释放窗体,dispose(),当最后一个窗口被释放后,则程序也随之运行结束。
        EXIT_ON_CLOSE,直接关闭应用程序,System.exit(0)。一个main函数对应一整个程序。
         */
        /* 窗口设置 */
        JFrame frame = new JFrame("Game 2048");
        frame.setSize(400, 530); // width * height
        frame.setResizable(false); // 窗口大小不可调整
        frame.setVisible(true); // true窗口可见
        frame.setLocationRelativeTo(null); // 窗口居中
        frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE); // 窗口的关闭
        frame.setLayout(null); // 设置用户界面上的屏幕组件的格式布局,默认为流式布局

        /* 添加标签 */
        // 添加标题: 2048
        JLabel ltitle = new JLabel("2048", JLabel.CENTER);
        frame.add(ltitle);
        ltitle.setFont(Data.title); // 标题字体/大小/位置
        ltitle.setForeground(Color.BLACK);
        ltitle.setBounds(50, 0, 150, 60);

        // 添加分数: 得分: 0
        JLabel lscorename = new JLabel("得 分", JLabel.CENTER);
        frame.add(lscorename);
        lscorename.setFont(Data.score); // "得分"字体/大小/位置
        lscorename.setForeground(Color.WHITE);
        lscorename.setOpaque(true); // 设置控件是否透明.true: 控件不透明; false: 控件透明.
        lscorename.setBackground(Color.GRAY);
        lscorename.setBounds(250, 0, 120, 30);

        lscore = new JLabel("0", JLabel.CENTER);
        frame.add(lscore);
        lscore.setFont(Data.score); // "得分"字体/大小/位置
        lscore.setForeground(Color.WHITE);
        lscore.setOpaque(true); // 设置控件是否透明.true: 控件不透明; false: 控件透明.
        lscore.setBackground(Color.GRAY);
        lscore.setBounds(250, 30, 120, 30);

        // 游戏说明:
        JLabel ltips = new JLabel("操作: ↑ ↓ ← →, 按esc键重新开始  ", JLabel.CENTER);
        frame.add(ltips);
        ltips.setFont(Data.tips); // "说明"字体/大小/位置
        ltips.setForeground(Color.DARK_GRAY);
        ltips.setBounds(0, 60, 400, 40);

        // 游戏面板:
        JPanel panel = new Game2048Panel();
        frame.add(panel);
        panel.setBounds(0, 100, 400, 400); // 面板绘制区域
        panel.setBackground(Color.GRAY);
        panel.setFocusable(true); //setFocusable设置组件是否可被选中
        // FlowLayout(流式布局): 组件按照加入的先后顺序按照设置的对齐方式从左向右排列,一行排满到下一行开始继续排列
        panel.setLayout(new FlowLayout());
        // 键盘监听
        frame.addKeyListener((KeyListener) panel);
    }

    public static void main(String[] args) {
        new Form();
    }
}

3、Chart.class

package Game2048_java;

import java.awt.Color;
import java.awt.Font;

public class Chart implements Data{
    /**
     * 方格类
     * @param value: 面板上的数字值
     */
    public int value;

    public Chart(){
        clear();
    }

    /** 清除面板 */
    public void clear(){
        value = 0;
    }

    /** 对应数字的字体颜色 */
    public Color getForeground(){
        /* 对应数字的字体颜色
        0: 0xcdc1b4, 同背景颜色相同,因此不会显示
        2, 4: BLACK, 黑色
        其他: WHITE, 白色 */
        return switch (value) {
            case 0 -> new Color(0xcdc1b4);
            case 2, 4 -> Color.BLACK;
            default -> Color.WHITE;
        };
    }

    /** 对应数字的背景颜色 */
    public Color getBackground(){
        /* 对应数字的背景颜色 */
        return switch (value){
            case 0 -> new Color(0xcdc1b4);
            case 2 -> new Color(0xeee4da);
            case 4 -> new Color(0xede0c8);
            case 8 -> new Color(0xf2b179);
            case 16 -> new Color(0xf59563);
            case 32 -> new Color(0xf67c5f);
            case 64 -> new Color(0xf65e3b);
            case 128 -> new Color(0xedcf72);
            case 256 -> new Color(0xedcc61);
            case 512 -> new Color(0xedc850);
            case 1024 -> new Color(0xedc53f);
            case 2048 -> new Color(0xedc22e);
            default -> new Color(0x248c51);
        };
    }

    /** 对应数字的字体大小 */
    public Font getChartFont(){
        return switch (value){
            case 0, 2, 4, 8 -> font1;
            case 16, 32, 64 -> font2;
            case 128, 256, 512 -> font3;
            default -> font4;
        };
    }
}

4、Game2048Panel

package Game2048_java;

import javax.swing.JPanel;

import java.awt.Graphics;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.FontMetrics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

// 游戏面板需要对键盘进行监听,因此需要实现接口 KeyListener 中的 keyPressed 方法
public class Game2048Panel extends JPanel implements Data, KeyListener {
    /* 变量定义 */
    private static int scores = 0;
    private Chart[][] charts = new Chart[4][4]; // 游戏面板
    private boolean isadd = true;  // 是否新增数字

    /** 构造方法 */
    public Game2048Panel() {
        initGame();  // 初始化
    }

    /** 按下某个键时调用此方法 */
    @Override
    public void keyPressed(KeyEvent e) {
        /* getKeyCode():键盘上每一个按钮都有对应码(Code),可用来查知用户按了什么键,返回当前按钮的数值
           getKeyChar():处理的是比较高层的事件,返回的是每欠敲击键盘后得到的字符(中文输入法下就是汉字)
           getKeyText():返回与此事件中的键关联的字符。比如getKeyText(e.getKeyCode())就返回你所按下的键盘*/

        switch (e.getKeyCode()){
            /* VK_ESCAPE: Esc
               VK_UP: ↑
               VK_DOWN: ↓
               VK_LEFT: ←
               VK_RIGHT: →
            */
            // 重新开始游戏
            case KeyEvent.VK_ESCAPE:
                System.out.println("esc");
                initGame(); // 重新开始游戏,游戏初始化
                break;
            // ↑
            case KeyEvent.VK_UP:
//                System.out.println("up");
                MoveUp();  // 向左移动
                creatChart();  // 随机生成数字
//                judgeGameOver(); // 判断游戏是否结束
                break;
            // ↓
            case KeyEvent.VK_DOWN:
//                System.out.println("down");
                MoveDown();  // 向右移动
                creatChart();  // 随机生成数字
//                judgeGameOver(); // 判断游戏是否结束
                break;
            // ←
            case KeyEvent.VK_LEFT:
//                System.out.println("left");
                MoveLeft();  // 向左移动
                creatChart();  // 随机生成数字
//                judgeGameOver(); // 判断游戏是否结束
                break;
            // →
            case KeyEvent.VK_RIGHT:
//                System.out.println("right");
                MoveRight();  // 向右移动
                creatChart();  // 随机生成数字
//                judgeGameOver(); // 判断游戏是否结束
                break;
            // others
            default:
                break;
        }
        // paint()方法用来绘制图形,repaint()方法用来重新绘制图像
        repaint();
    }

    /** 游戏初始化 */
    private void initGame(){
        scores = 0;
        System.out.println("initGame");
        for (int row = 0; row < 4; row++) {
            for (int col = 0; col < 4; col++) {
                charts[row][col] = new Chart();  // from Chart.java
            }
        }
        // 随机生成两个数
        for (int i = 0; i < 2; i++){
            isadd = true;
            creatChart();  // 随机生成一个数字
        }
    }

    /** 随机在一个位置生成一个数 */
    private void creatChart(){
        List<Chart> list = getEmptyCharts();  // list 为空白方格

        if (!list.isEmpty() && isadd) {  // 在空白方格出随机生成一个数字
            Random random = new Random();
            int index = random.nextInt(list.size());
            Chart chart = list.get(index);
            // 2, 4出现概率3:1
            /* random.nextInt(int n) 是参数 [0, n) 的随机数 */
            /* random.nextInt(4): 随机生成 0, 1, 2, 3; */
            chart.value = (random.nextInt(4) % 3 == 0) ? 2 : 4;
            System.out.println(chart.value);
            isadd = false;
        }
    }

    /** 获取空白方格 */
    private List<Chart> getEmptyCharts(){
        List<Chart> chartList = new ArrayList<>();
        for (int i = 0; i < 4; i++){
            for (int j = 0; j < 4; j++){
                if (charts[i][j].value == 0){
                    chartList.add(charts[i][j]);  //添加元素到 ArrayList 可以使用 add() 方法
                }
            }
        }
        return chartList;
    }

    /** 向上移动 */
    private boolean MoveUp() {
//        System.out.println("MoveUp");
        /* 向上移动,只需考虑第二行到第四行
           共分为两种情况:
           1、当前数字上边无空格,即上边值不为 0
              a. 当前数字与上边数字相等,合并
              b. 当前数字与上边数字不相等,continue
           2、当前数字上边有空格,即上边值为 0, 上移 */
        for (int j = 0; j < 4; j++) {
            for (int i = 1, index = 0; i < 4; i++) {
                if (charts[i][j].value > 0) {
                    if (charts[i][j].value == charts[index][j].value) {
                        // 当前数字 == 上边数字
                    /* 分数: 当前数字 + 上边数字
                       数值: 上边数字 = 上边数字 + 当前数字, 当前数字 = 0 */
                        scores += charts[i][j].value + charts[index][j].value;
                        charts[index][j].value = charts[i][j].value + charts[index][j].value;
                        charts[i][j].value = 0;
                        index += 1;
                        isadd = true;
                    }
                    // 当前数字与上边数字不相等,continue 可以省略不写
                    else if (charts[index][j].value == 0) {
                        // 当前数字上边有0
                    /* 分数: 不变
                       数值: 上边数字 = 当前数字, 当前数字 = 0 */
                        charts[index][j].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                    else if (charts[++index][j].value == 0) {
                        // index 相当于慢指针,j 相当于快指针
                        // 也就是说快指针和慢指针中间可能存在一个以上的空格,或者index和j并未相邻
                        // 上边数字 = 0
                    /* 分数: 不变
                       数值: 上边数字 = 当前数字, 当前数字 = 0 */
                        charts[index][j].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                }
            }
        }
        return isadd;
    }

    /** 向下移动 */
    private boolean MoveDown(){
        System.out.println("MoveDown");
        /* 向下移动,只需考虑第一列到第三列
           共分为两种情况:
           1、当前数字下边无空格,即下边值不为 0
              a. 当前数字与下边数字相等,合并
              b. 当前数字与下边数字不相等,continue
           2、当前数字下边有空格,即下边值为 0, 下移 */
        for (int j = 0; j < 4; j++) {
            for (int i = 2, index = 3; i >= 0; i--) {
                if (charts[i][j].value > 0) {
                    if (charts[i][j].value == charts[index][j].value) {
                        // 当前数字 == 下边数字
                        /* 分数: 当前数字 + 下边数字
                           数值: 下边数字 = 下边数字 + 当前数字, 当前数字 = 0 */
                        scores += charts[i][j].value + charts[index][j].value;
                        charts[index][j].value = charts[i][j].value + charts[index][j].value;
                        charts[i][j].value = 0;
                        index -= 1;
                        isadd = true;
                    }
                    // 当前数字与下边数字不相等,continue 可以省略不写
                    else if (charts[index][j].value == 0) {
                        // 当前数字下边有0
                        /* 分数: 不变
                           数值: 下边数字 = 当前数字, 当前数字 = 0 */
                        charts[index][j].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                    else if (charts[--index][j].value == 0) {
                        // index 相当于慢指针,j 相当于快指针
                        // 也就是说快指针和慢指针中间可能存在一个以上的空格,或者index和j并未相邻
                        // 下边数字 = 0
                        /* 分数: 不变
                           数值: 下边数字 = 当前数字, 当前数字 = 0 */
                        charts[index][j].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                }
            }
        }
        return isadd;
    }

    /** 向左移动 */
    private boolean MoveLeft(){
    //        System.out.println("MoveLeft");
        /* 向左移动,只需考虑第二列到第四列
           共分为两种情况:
           1、当前数字左边无空格,即左边值不为 0
              a. 当前数字与左边数字相等,合并
              b. 当前数字与左边数字不相等,continue
           2、当前数字左边有空格,即左边值为 0, 左移 */
        for (int i = 0; i < 4; i++) {
            for (int j = 1, index = 0; j < 4; j++) {
                if (charts[i][j].value > 0) {
                    if (charts[i][j].value == charts[i][index].value) {
                        // 当前数字 == 左边数字
                        /* 分数: 当前数字 + 左边数字
                           数值: 左边数字 = 左边数字 + 当前数字, 当前数字 = 0 */
                        scores += charts[i][j].value + charts[i][index].value;
                        charts[i][index].value = charts[i][index].value + charts[i][j].value;
                        charts[i][j].value = 0;
                        index += 1;
                        isadd = true;
                    }
                    // 当前数字与左边数字不相等,continue 可以省略不写
                    else if (charts[i][index].value == 0) {
                        // 当前数字左边有0
                        /* 分数: 不变
                           数值: 左边数字 = 当前数字, 当前数字 = 0 */
                        charts[i][index].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                    else if (charts[i][++index].value == 0) {
                        // index 相当于慢指针,j 相当于快指针
                        // 也就是说快指针和慢指针中间可能存在一个以上的空格,或者index和j并未相邻
                        // 左边数字 = 0
                        /* 分数: 不变
                           数值: 左边数字 = 当前数字, 当前数字 = 0 */
                        charts[i][index].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                }
            }
        }

        return isadd;
    }

    /** 向右移动 */
    private boolean MoveRight(){
    //        System.out.println("MoveRight");
      /* 向右移动,只需考虑第一列到第三列
         共分为两种情况:
         1、当前数字右边无空格,即右边值不为 0
            a. 当前数字与右边数字相等,合并
            b. 当前数字与右边数字不相等,continue
         2、当前数字右边有空格,即右边值为 0, 右移 */
        for (int i = 0; i < 4; i++) {
            for (int j = 2, index = 3; j >= 0; j--) {
                if (charts[i][j].value > 0) {
                    if (charts[i][j].value == charts[i][index].value) {
                        // 当前数字 == 右边数字
                        /* 分数: 当前数字 + 右边数字
                           数值: 右边数字 = 右边数字 + 当前数字, 当前数字 = 0 */
                        scores += charts[i][j].value + charts[i][index].value;
                        charts[i][index].value = charts[i][j].value + charts[i][index].value;
                        charts[i][j].value = 0;
                        index -= 1;
                        isadd = true;
                    }
                    // 当前数字与左边数字不相等,continue 可以省略不写
                    else if (charts[i][index].value == 0) {
                        // 当前数字右边有0
                        /* 分数: 不变
                           数值: 右边数字 = 当前数字, 当前数字 = 0 */
                        charts[i][index].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                    else if (charts[i][--index].value == 0) {
                        // index 相当于慢指针,j 相当于快指针
                        // 也就是说快指针和慢指针中间可能存在一个以上的空格,或者index和j并未相邻
                        // 右边数字 = 0
                        /* 分数: 不变
                           数值: 右边数字 = 当前数字, 当前数字 = 0 */
                        charts[i][index].value = charts[i][j].value;
                        charts[i][j].value = 0;
                        isadd = true;
                    }
                }
            }
        }
        return isadd;
    }

    /** 判断游戏是否结束 */
    private boolean judgeGameOver(){
        // 将lscore标签内容设置为 scores + ""
        Form.lscore.setText(scores + "");

        // 当空白空格不为空时,即游戏未结束
        if (!getEmptyCharts().isEmpty()){
            return false;
        }

        // 当空白方格为空时,判断是否存在可合并的方格
        for (int i = 0; i < 3; i++){
            for (int j = 0; j < 3; j++){
                if (charts[i][j].value == charts[i][j + 1].value
                    || charts[i][j].value == charts[i + 1][j].value){
                    return false;
                }
            }
        }
        // 若不满足以上两种情况,则游戏结束
        return true;
    }

    /** 判断游戏是否成功 */
    private boolean judgeGameSuccess() {
        // 检查是否有2048
        for (int i = 0; i< 4; i++) {
            for (int j = 0; j < 4; j++) {
                if (charts[i][j].value == 2048) {
                    return true;
                }
            }
        }
        return false;
    }

    /** 画笔函数 */
    @Override
    public void paint(Graphics g){
        super.paint(g);
        System.out.println("paint");
        for (int i = 0; i < 4; i++){
            for (int j = 0; j < 4; j++){
                drawChart(g, i, j);
            }
        }

        // 如果游戏结束
        if (judgeGameOver()){
            g.setColor(new Color(64, 64, 64, 150));
            g.fillRect(0, 0, getWidth(), getHeight());  // 画矩形
            g.setColor(Color.WHITE);  // 画笔颜色为白色
            g.setFont(title);
            FontMetrics fm = getFontMetrics(title);
            String value = "Game Over!";  // 内容: Game Over!
            g.drawString(value,
                    (getWidth() - fm.stringWidth(value)) / 2,
                    getHeight() / 2);
        }  // 位置

        // 如果游戏成功
        if (judgeGameSuccess()) {
            g.setColor(new Color(64, 64, 64, 150));
            g.fillRect(0, 0, getWidth(), getHeight());  // 画矩形
            g.setColor(Color.RED);  // 画笔颜色为红色
            g.setFont(title);
            FontMetrics fm = getFontMetrics(title);
            String value = "Successful!";  // 内容: Successful!
            g.drawString(value,
                    (getWidth() - fm.stringWidth(value)) / 2,
                    getHeight() / 2);
          // 位置
        }
    }

    /** 绘制方格 */
    private void drawChart(Graphics g, int i, int j){
        /* Java语言在Graphics类提供绘制各种基本的几何图形的基础上,
           扩展Graphics类提供一个Graphics2D类,
           它拥用更强大的二维图形处理能力,提供、坐标转换、颜色管理以及文字布局等更精确的控制。*/
        Graphics2D g2d = (Graphics2D) g;
        /* setRenderingHint() 方法的参数是一个键以及对应的键值。
           KEY_ANTIALIASING: 抗锯齿提示键。对象的几何形状呈现方法是否将尝试沿形状的边缘减少锯齿现象
           此提示允许的值有:
           -- VALUE_ANTIALIAS_ON:使用抗锯齿模式完成呈现
           -- VALUE_ANTIALIAS_OFF:在不使用抗锯齿模式的情况下完成呈现
           -- VALUE_ANTIALIAS_DEFAULT:使用由实现选择的默认抗锯齿模式完成呈现
           KEY_STROKE_CONTROL: 笔划规范化控制提示键。STROKE_CONTROL 提示键控制呈现实现是否应该或允许出于各种目的而修改所呈现轮廓的几何形状。
           此提示允许的值有
           -- VALUE_STROKE_NORMALIZE:几何形状应当规范化,以提高均匀性或直线间隔和整体美观。
           -- VALUE_STROKE_PURE:几何形状应该保持不变并使用子像素精确度呈现
           -- VALUE_STROKE_DEFAULT:根据给定实现的权衡,可以修改几何形状或保留原来的几何形状。
           */
        // 消除锯齿
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        // 几何形状规范化
        g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                RenderingHints.VALUE_STROKE_NORMALIZE);

        Chart chart = charts[i][j];
        g2d.setColor(chart.getBackground());  // 表格背景颜色
        /* 绘制圆角
           -- x: 填充矩形的 x 坐标
           -- y: 填充矩形的 y 坐标
           -- width: 填充矩形的宽度
           -- height: 填充矩形的高度
           -- arcwidth: 4个弧度的水平直径
           -- archeight: 4个弧度的垂直直径 */
        g2d.fillRoundRect(CHART_GAP + (CHART_GAP + CHART_SIZE) * j,
                CHART_GAP + (CHART_GAP + CHART_SIZE) * i,
                CHART_SIZE, CHART_SIZE, CHART_ARC, CHART_ARC);
        g2d.setColor(chart.getForeground());  // 表格前景颜色
        g2d.setFont(chart.getChartFont());   // 设置字体

        // 文字设定
        /* FontMetrics 字体属性类
           GetAscent(): Ascent表示字体从基线到顶端的距离
           getDescent(): Descent表示字体从基线到下降字符底端的距离
           getLeading(): Leading 表示本文行之间的距离
           getheight(): 字体高度  Ascent + Descent + Leading
           StringWidth(String): 字符串宽度 */
        FontMetrics fm = getFontMetrics(chart.getChartFont());
        String value = String.valueOf(chart.value);  // int型转换为String字符串
        g2d.drawString(value,
                CHART_GAP + (CHART_GAP + CHART_SIZE) * j +
                        (CHART_SIZE - fm.stringWidth(value)) / 2,
                CHART_GAP + (CHART_GAP + CHART_SIZE) * i +
                        (CHART_SIZE - fm.getAscent() - fm.getDescent()) / 2
                        + fm.getAscent());

    }

    /** 释放某个键时调用此方法 */
    @Override
    public void keyReleased(KeyEvent e) {

    }

    /** 键入某个键时调用此方法 */
    @Override
    public void keyTyped(KeyEvent e) {

    }
}

参考:

猜你喜欢

转载自blog.csdn.net/weixin_45666660/article/details/113601912
今日推荐