Java实现贪吃蛇能玩出什么花样?桌面图标版的体验一下

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!

前言

如果问你,桌面图标除了双击启动程序外还可以干嘛?

当然是当游戏玩了,所以你就会看到下面这幅图。

Java实现贪吃蛇能玩出什么花样?桌面图标版的体验一下

875na-3i19d.gif

下载地址:http://houxinlin.com:8080/Game.rar

注意这个地址的服务器是前几章使用汇编实现的,想看看现实中是否可以简单支撑,如果无法访问,请留言。

下面说如何实现。

实现原理

本文需要了解的东西居多,看官看看就行,如需详细了解,需要先了解Windows开发,本文不会详细说每个知识点。

这里面有两个问题是java实现不了的,也就是注册热键和移动桌面图标,所以这部分用c来实现,下面是定义的一些jni接口。

public class GameJNI {

    public native void registerHotkeyCallback(HotkeyCallback callback);

    public native  int getDesktopIcon();

    public native void moveDesktopIcon(int index,int x,int y);
}

如果不懂Windows机制,可能会无法理解到底怎么移动桌面图标,简短的说一下,Windows是基于消息机制的,消息告诉某个窗口发生了什么事情,比如当你注册热键后,这个热键被按下,那么在Windows的窗口函数中就会得到响应,窗口函数中会得到具体消息的标识符,热键的标识符是WM_HOTKEY,而还有其他一同由系统传递过来的参数如wParam、lParam。

而移动图标就是向图标所在的窗口发送一个消息,表示有人想重新设置某个图标位置。

首先看registerHotkeyCallback的实现。


void RegisterHotkey(int id,int vkCode) {
 if (RegisterHotKey(NULL, id, 0, vkCode) == 0) {
            printf("fail %dn", vkCode);
 }
}
JNIEXPORT void JNICALL Java_com_h_game_jni_GameJNI_registerHotkeyCallback
(JNIEnv *env, jobject thisObj, jobject callbackObject) 
{

 RegisterHotkey(LEFT_ID,37);
 RegisterHotkey(UP_ID,38);
 RegisterHotkey(RIGHT_ID,39);
 RegisterHotkey(DOWN_ID,40);
 fflush(stdout);

 MSG lpMsg = {0};
 while (GetMessage(&lpMsg, NULL, 00)!=0){
  jclass thisClass = (*env)->GetObjectClass(env, callbackObject);
  jmethodID hotKeyCallbackMethodId = (*env)->GetMethodID(env, thisClass, "hotkey""(I)V");
  if (NULL == hotKeyCallbackMethodId) return;
  if (lpMsg.message== WM_HOTKEY){
   jstring result = (jstring)(*env)->CallObjectMethod(env, callbackObject, hotKeyCallbackMethodId, lpMsg.wParam);
  }
 
 }

}

这部分代码是最多的,首先通过RegisterHotKey函数注册4个热键,分别是←↑→↓,并给他们起一个id分别为1、2、3、4,然后不断通过GetMessage从消息队列获取消息,GetMessage通常都是在窗体应用中使用的,而此时对于他来说,没有窗体,所以第二个参数为NULL,第一个参数是队列中到达的消息信息,当消息是WM_HOTKEY,表示我们按下了定义的热键,在通过java为我们提供的交互API,拿到回调地址,进行调用。

在看getDesktopIcon的实现,用来获取桌面图标个数,桌面也可以当做一个窗口,而桌面中排列图标的是一个ListView,当这个ListView收到LVM_GETITEMCOUNT消息时候,表示有人想获取他的的图标个数,然后他会返回给我们,而Windows提供了SendMessage函数,用来给指定窗口发送一个消息,并且是有返回值的,还有一个用于无返回值函数PostMessage,下面会说。

int GetDesktopIconCount() {
 return  SendMessage(GetWindowHwnd(), LVM_GETITEMCOUNT, 00);
}
JNIEXPORT jint JNICALL Java_com_h_game_jni_GameJNI_getDesktopIcon
(JNIEnv *env, jobject thisObj) 
{
 return GetDesktopIconCount();
}

而要想得到桌面中ListView得句柄,需要这样做,但这段代码我不确定是否能在Win11上正常运行,因为每个系统中桌面结构可能不一样,Win7中和Win10就是不一样的,如果Win11中没有更改这个结构,这段代码会正常运行。

int GetWindowHwnd() {
 HWND hwndWorkerW = { 0 };
 hwndWorkerW = FindWindow(NULL,TEXT("Program Manager"));
 hwndWorkerW = FindWindowEx(hwndWorkerW, 0, TEXT("SHELLDLL_DefView"), NULL);
 hwndWorkerW = FindWindowEx(hwndWorkerW, 0,TEXT("SysListView32"), TEXT("FolderView"));
 return hwndWorkerW;
}

这里又牵扯到很多知识点,句柄和查找句柄,句柄是一个整数,用来标识唯一的一个应用窗口,查找句柄就可以根据类名或者窗口标题名进行查找,而这里会包含子父级关系,详细需要了解CreateWindow函数。

下面是移动图标实现,当那个ListView收到LVM_SETITEMPOSITION消息后,会根据其他参数设置位置,而这里使用PostMessage投递消息是因为我们不需要返回值,如果使用SendMessage,会慢很多。

void MoveDesktopIcon(int iconIndex,int x,int y) {
 PostMessage(GetWindowHwnd(), LVM_SETITEMPOSITION, iconIndex, ConversionXY(x, y));
}
int ConversionXY(int  x, int  y) {
 return  y * 65536 + x;
}
JNIEXPORT void JNICALL Java_com_h_game_jni_GameJNI_moveDesktopIcon
(JNIEnv *env, jobject thisObj, jint index, jint x, jint y) 
{
 MoveDesktopIcon(index, x, y);
}


好了下面是java代码实现。

public class Main {
    static {
        System.load("Game.dll");
    }

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

这部分比较简单了,没什么好说的,由c已经提供好了基本对图标的操作,这里直接调用即可。

注意这里我们拿图标索引0当作头,索引最后一个当做食物,比如有10个图标,0是头,9是食物,1-8是身体,每当根据方向移动头时,先记录下移动之前的位置,当头移动后,把他记录的位置传递给第1个图标,然后在把索引1的原来位置传递给第2个,依次类推,就形成了贪吃蛇。

public class SnakeGame extends JFrame implements HotkeyCallback {
    private static final int WINDOW_WIDTH = 300;
    private static final int WINDOW_HEIGHT = 200;
    private GameJNI gameJNI = new GameJNI();
    private ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1);
    private Direction direction = Direction.RIGHT;
    private Point snakePoint = new Point(00);
    private static final int MOVE_SIZE = 84;
    private List<Point> snakeBodyPoint = new ArrayList<>();
    private Point foodPoint = new Point(00);
    private List<Point> allPoint = generatorPoints();
    private ScheduledFuture<?> scheduledFuture = null;

    public SnakeGame() {
        this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        this.setLocation(screenSize.width / 2 - WINDOW_WIDTH / 2, screenSize.height / 2 - WINDOW_HEIGHT / 2);
        this.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
        init();
        this.setVisible(true);
    }

    private void startGame() {
        this.setVisible(false);
        if (scheduledFuture == null) {
            scheduledFuture = scheduledExecutorService.scheduleAtFixedRate(() -> {
                Point oldPoint = snakePoint.getLocation();
                if (direction == Direction.LEFT) snakePoint.x -= MOVE_SIZE;
                if (direction == Direction.UP) snakePoint.y -= MOVE_SIZE;
                if (direction == Direction.RIGHT) snakePoint.x += MOVE_SIZE;
                if (direction == Direction.DOWN) snakePoint.y += MOVE_SIZE;
                moveSnakeHeader();
                moveSnakeBody(oldPoint);
                isCollision();
            }, 0200, TimeUnit.MILLISECONDS);
        }

    }


    private void isCollision() {
        if (snakeBodyPoint.stream().anyMatch(point -> point.equals(snakePoint))) {
            scheduledFuture.cancel(true);
            scheduledFuture = null;
            this.setVisible(true);
        }
    }

    private void moveSnakeHeader() {
        gameJNI.moveDesktopIcon(0, snakePoint.x, snakePoint.y);
        if (foodPoint.equals(snakePoint)) resetBodyLocation();
    }

    private List<Point> generatorPoints() {
        List<Point> all = new ArrayList<>();
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        for (int i = 0; i < screenSize.width; i += MOVE_SIZE) {
            for (int j = 0; j < screenSize.height; j += MOVE_SIZE) {
                all.add(new Point(i, j));
            }
        }
        return all;
    }

    private void resetBodyLocation() {
        List<Point> newPoint = allPoint.stream().filter(point -> !has(point)).collect(Collectors.toList());
        Collections.shuffle(newPoint);
        Point point = newPoint.get(0);
        int desktopIcon = gameJNI.getDesktopIcon();
        foodPoint.setLocation(point.x, point.y);
        gameJNI.moveDesktopIcon(desktopIcon - 1, point.x, point.y);
    }

    private boolean has(Point hasPoint) {
        return snakeBodyPoint.stream().anyMatch(point -> hasPoint.equals(point));
    }

    private void moveSnakeBody(Point oldPoint) {
        for (int i = 1; i < snakeBodyPoint.size() - 1; i++) {
            Point itemPoint = snakeBodyPoint.get(i);
            gameJNI.moveDesktopIcon(i, oldPoint.x, oldPoint.y);
            snakeBodyPoint.set(i, oldPoint.getLocation());
            oldPoint = itemPoint;
        }
    }


    private void init() {
        this.setLayout(new BorderLayout());
        String str ="<html>首先右击桌面,查看>取消自动排列图片、将网格与图片对齐。方向键为← ↑ → ↓</html>";
        JLabel jLabel = new JLabel(str);
        jLabel.setFont(new Font("黑体",0,18));
        add(jLabel, BorderLayout.NORTH);
        add(createButton(), BorderLayout.SOUTH);
        registerHotkey();
        reset();
    }

    private void reset() {
        snakeBodyPoint.clear();
        direction = Direction.RIGHT;
        snakePoint.setLocation(00);
        int desktopIcon = gameJNI.getDesktopIcon();
        int offsetX = -MOVE_SIZE;
        for (int i = 0; i < desktopIcon; i++) {
            snakeBodyPoint.add(new Point(offsetX, 0));
            gameJNI.moveDesktopIcon(i, offsetX, 0);
            offsetX -= MOVE_SIZE;
        }
        resetBodyLocation();
    }

    private JButton createButton() {


        JButton jButton = new JButton("开始");
        jButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                reset();
                startGame();
            }
        });
        return jButton;
    }

    @Override
    public void hotkey(int key) {
        if (key == 1) direction = Direction.LEFT;
        if (key == 2) direction = Direction.UP;
        if (key == 3) direction = Direction.RIGHT;
        if (key == 4) direction = Direction.DOWN;

    }

    public void registerHotkey() {
        new Thread(() -> gameJNI.registerHotkeyCallback(this)).start();
    }

    enum Direction {
        LEFT, UP, RIGHT, DOWN
    }
}

- END -


原文始发于微信公众号(十四个字节):Java实现贪吃蛇能玩出什么花样?桌面图标版的体验一下