ZetCode

Java 拼图游戏

最后修改于 2023 年 1 月 10 日

在本 Java 游戏教程部分,我们将创建一个 Java 拼图游戏克隆。源代码和图像可以在作者的 Github Puzzle-game-in-Java-Swing 存储库中找到。

Java 拼图游戏要点

Java 拼图游戏示例

这个小游戏的目标是组成一幅图片。包含图片的按钮可以通过点击来移动。只有与空白按钮相邻的按钮才能被移动。

PuzzleEx.java
package com.zetcode;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.awt.image.CropImageFilter;
import java.awt.image.FilteredImageSource;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

class MyButton extends JButton {

    private boolean isLastButton;

    public MyButton() {

        super();

        initUI();
    }

    public MyButton(Image image) {

        super(new ImageIcon(image));

        initUI();
    }

    private void initUI() {

        isLastButton = false;
        BorderFactory.createLineBorder(Color.gray);

        addMouseListener(new MouseAdapter() {

            @Override
            public void mouseEntered(MouseEvent e) {
                setBorder(BorderFactory.createLineBorder(Color.yellow));
            }

            @Override
            public void mouseExited(MouseEvent e) {
                setBorder(BorderFactory.createLineBorder(Color.gray));
            }
        });
    }

    public void setLastButton() {
        
        isLastButton = true;
    }

    public boolean isLastButton() {

        return isLastButton;
    }
}

public class PuzzleEx extends JFrame {

    private JPanel panel;
    private BufferedImage source;
    private BufferedImage resized;    
    private Image image;
    private MyButton lastButton;
    private int width, height;    
    
    private List<MyButton> buttons;
    private List<Point> solution;

    private final int NUMBER_OF_BUTTONS = 12;
    private final int DESIRED_WIDTH = 300;

    public PuzzleEx() {

        initUI();
    }

    private void initUI() {

        solution = new ArrayList<>();
        
        solution.add(new Point(0, 0));
        solution.add(new Point(0, 1));
        solution.add(new Point(0, 2));
        solution.add(new Point(1, 0));
        solution.add(new Point(1, 1));
        solution.add(new Point(1, 2));
        solution.add(new Point(2, 0));
        solution.add(new Point(2, 1));
        solution.add(new Point(2, 2));
        solution.add(new Point(3, 0));
        solution.add(new Point(3, 1));
        solution.add(new Point(3, 2));

        buttons = new ArrayList<>();

        panel = new JPanel();
        panel.setBorder(BorderFactory.createLineBorder(Color.gray));
        panel.setLayout(new GridLayout(4, 3, 0, 0));

        try {
            source = loadImage();
            int h = getNewHeight(source.getWidth(), source.getHeight());
            resized = resizeImage(source, DESIRED_WIDTH, h,
                    BufferedImage.TYPE_INT_ARGB);

        } catch (IOException ex) {
            Logger.getLogger(PuzzleEx.class.getName()).log(
                    Level.SEVERE, null, ex);
        }

        width = resized.getWidth(null);
        height = resized.getHeight(null);

        add(panel, BorderLayout.CENTER);

        for (int i = 0; i < 4; i++) {

            for (int j = 0; j < 3; j++) {

                image = createImage(new FilteredImageSource(resized.getSource(),
                        new CropImageFilter(j * width / 3, i * height / 4,
                                (width / 3), height / 4)));
                
                MyButton button = new MyButton(image);
                button.putClientProperty("position", new Point(i, j));

                if (i == 3 && j == 2) {
                    lastButton = new MyButton();
                    lastButton.setBorderPainted(false);
                    lastButton.setContentAreaFilled(false);
                    lastButton.setLastButton();
                    lastButton.putClientProperty("position", new Point(i, j));
                } else {
                    buttons.add(button);
                }
            }
        }

        Collections.shuffle(buttons);
        buttons.add(lastButton);

        for (int i = 0; i < NUMBER_OF_BUTTONS; i++) {

            MyButton btn = buttons.get(i);
            panel.add(btn);
            btn.setBorder(BorderFactory.createLineBorder(Color.gray));
            btn.addActionListener(new ClickAction());
        }

        pack();
        setTitle("Puzzle");
        setResizable(false);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    private int getNewHeight(int w, int h) {

        double ratio = DESIRED_WIDTH / (double) w;
        int newHeight = (int) (h * ratio);
        return newHeight;
    }

    private BufferedImage loadImage() throws IOException {

        BufferedImage bimg = ImageIO.read(new File("src/resources/icesid.jpg"));

        return bimg;
    }

    private BufferedImage resizeImage(BufferedImage originalImage, int width,
            int height, int type) throws IOException {

        BufferedImage resizedImage = new BufferedImage(width, height, type);
        Graphics2D g = resizedImage.createGraphics();
        g.drawImage(originalImage, 0, 0, width, height, null);
        g.dispose();

        return resizedImage;
    }

    private class ClickAction extends AbstractAction {

        @Override
        public void actionPerformed(ActionEvent e) {

            checkButton(e);
            checkSolution();
        }

        private void checkButton(ActionEvent e) {

            int lidx = 0;
            
            for (MyButton button : buttons) {
                if (button.isLastButton()) {
                    lidx = buttons.indexOf(button);
                }
            }

            JButton button = (JButton) e.getSource();
            int bidx = buttons.indexOf(button);

            if ((bidx - 1 == lidx) || (bidx + 1 == lidx)
                    || (bidx - 3 == lidx) || (bidx + 3 == lidx)) {
                Collections.swap(buttons, bidx, lidx);
                updateButtons();
            }
        }

        private void updateButtons() {

            panel.removeAll();

            for (JComponent btn : buttons) {

                panel.add(btn);
            }

            panel.validate();
        }
    }

    private void checkSolution() {

        List<Point> current = new ArrayList<>();

        for (JComponent btn : buttons) {
            current.add((Point) btn.getClientProperty("position"));
        }

        if (compareList(solution, current)) {
            JOptionPane.showMessageDialog(panel, "Finished",
                    "Congratulation", JOptionPane.INFORMATION_MESSAGE);
        }
    }

    public static boolean compareList(List ls1, List ls2) {
        
        return ls1.toString().contentEquals(ls2.toString());
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                PuzzleEx puzzle = new PuzzleEx();
                puzzle.setVisible(true);
            }
        });
    }
}

我们使用了《冰川时代》电影中 Sid 角色的图像。我们缩放图像并将其切割成十二块。这些块由 JButton 组件使用。最后一块不使用;我们有一个空白按钮代替。您可以下载一张相当大的图片并在该游戏中使用。

addMouseListener(new MouseAdapter() {

    @Override
    public void mouseEntered(MouseEvent e) {
        setBorder(BorderFactory.createLineBorder(Color.yellow));
    }

    @Override
    public void mouseExited(MouseEvent e) {
        setBorder(BorderFactory.createLineBorder(Color.gray));

    }
});

当我们将鼠标指针悬停在按钮上时,其边框会变成黄色。

public boolean isLastButton() {

    return isLastButton;
}

有一个按钮我们称之为最后一个按钮。这是一个没有图片的按钮。其他按钮与它交换位置。

private final int DESIRED_WIDTH = 300;

我们用来组成的图像被缩放到所需的宽度。使用 getNewHeight() 方法,我们在保持图像比例的同时计算新高度。

solution.add(new Point(0, 0));
solution.add(new Point(0, 1));
solution.add(new Point(0, 2));
solution.add(new Point(1, 0));
...

解决方案列表存储了组成图像的按钮的正确顺序。每个按钮都由一个 Point 标识。

panel.setLayout(new GridLayout(4, 3, 0, 0));

我们使用 GridLayout 来存储我们的组件。布局包含 4 行和 3 列。

image = createImage(new FilteredImageSource(resized.getSource(),
        new CropImageFilter(j * width / 3, i * height / 4,
                (width / 3), height / 4)));

CropImageFilter 用于从已调整大小的图像源中裁剪矩形区域。它旨在与 FilteredImageSource 对象结合使用,以生成现有图像的裁剪版本。

button.putClientProperty("position", new Point(i, j));

按钮由它们的 position 客户端属性标识。这是一个包含按钮在图片中正确的行和列位置的点。这些属性用于确定我们窗口中的按钮顺序是否正确。

if (i == 3 && j == 2) {
    lastButton = new MyButton();
    lastButton.setBorderPainted(false);
    lastButton.setContentAreaFilled(false);
    lastButton.setLastButton();
    lastButton.putClientProperty("position", new Point(i, j));
} else {
    buttons.add(button);
}

没有图片的按钮称为最后一个按钮;它位于网格的右下角。它是与被点击的相邻按钮交换位置的按钮。我们使用 setLastButton() 方法设置其 isLastButton 标志。

Collections.shuffle(buttons);
buttons.add(lastButton);

我们随机重排 buttons 列表的元素。最后一个按钮,即没有图片的按钮,被插入到列表的末尾。它不应该被洗牌,当我们在游戏开始时,它总是放在最后。

for (int i = 0; i < NUMBER_OF_BUTTONS; i++) {

    MyButton btn = buttons.get(i);
    panel.add(btn);
    btn.setBorder(BorderFactory.createLineBorder(Color.gray));
    btn.addActionListener(new ClickAction());
}

buttons 列表中的所有组件都放置在面板上。我们在按钮周围创建了一些灰色边框,并添加了点击动作监听器。

private int getNewHeight(int w, int h) {

    double ratio = DESIRED_WIDTH / (double) w;
    int newHeight = (int) (h * ratio);
    return newHeight;
}

getNewHeight() 方法根据所需的宽度计算图像的高度。保持图像的比例。我们使用这些值来缩放图像。

private BufferedImage loadImage() throws IOException {

    BufferedImage bimg = ImageIO.read(new File("src/resources/icesid.jpg"));

    return bimg;
}

JPG 图像从磁盘加载。ImageIOread() 方法返回一个 BufferedImage,这是 Swing 用于处理图像的重要类。

private BufferedImage resizeImage(BufferedImage originalImage, int width,
        int height, int type) throws IOException {

    BufferedImage resizedImage = new BufferedImage(width, height, type);
    Graphics2D g = resizedImage.createGraphics();
    g.drawImage(originalImage, 0, 0, width, height, null);
    g.dispose();

    return resizedImage;
}

通过创建具有新尺寸的新 BufferedImage 来调整原始图像的大小。我们将原始图像绘制到这个新的缓冲图像中。

private void checkButton(ActionEvent e) {

    int lidx = 0;
    for (MyButton button : buttons) {
        if (button.isLastButton()) {
            lidx = buttons.indexOf(button);
        }
    }

    JButton button = (JButton) e.getSource();
    int bidx = buttons.indexOf(button);

    if ((bidx - 1 == lidx) || (bidx + 1 == lidx)
            || (bidx - 3 == lidx) || (bidx + 3 == lidx)) {
        Collections.swap(buttons, bidx, lidx);
        updateButtons();
    }
}

按钮存储在一个列表中。然后将此列表映射到面板的网格。我们获取最后一个按钮和被点击按钮的索引。如果它们相邻,则使用 Collections.swap() 交换它们。

private void updateButtons() {

    panel.removeAll();

    for (JComponent btn : buttons) {

        panel.add(btn);
    }

    panel.validate();
}

updateButtons() 方法将列表映射到面板的网格。首先,所有组件都使用 removeAll() 方法移除。然后使用 for 循环遍历 buttons 列表,将重排后的按钮添加回面板的布局管理器。最后,validate() 方法实现新布局。

private void checkSolution() {

    List<Point> current = new ArrayList<>();

    for (JComponent btn : buttons) {
        current.add((Point) btn.getClientProperty("position"));
    }

    if (compareList(solution, current)) {
        JOptionPane.showMessageDialog(panel, "Finished",
                "Congratulation", JOptionPane.INFORMATION_MESSAGE);
    }
}

通过将正确排序按钮的点列表与当前包含窗口中按钮顺序的列表进行比较来完成解决方案检查。如果达到了解决方案,则会显示一个消息对话框。

Puzzle
图:组成图像

这是一个用 Java 创建的拼图游戏。