跳一跳——电脑能做的事就不要人工来做啦

补发30日未发的博客。
PS:最新改进的算法和针对我自己手机调优的参数已经停不下来了,见项目:https://github.com/GameTerminator/AutoJump

几年前参考网上的文章写过天天连萌自动玩的项目(之前写在 iteye 的博客上:http://maosidiaoxian.iteye.com,github 项目地址为:https://github.com/GameTerminator/lianmeng),这次微信小游戏里的跳一跳玩了玩,就自然而然地想到用同样的方式来做。

经过几次修正和简化,最终思路和实现如下:

  1. 使用 monkeyrunner 里的接口截图
  2. 找出跳动的那个小球的 x 坐标
  3. 找出最终要跳达的点的 x 坐标
  4. 算出其距离,并按线性方程计算出时间
  5. 使用 monkeyrunner 里的接口模拟长按事件

接下来是完整的 Java 代码实现过程。

截图及模拟长按

要调用 monkeyrunner 来截图及模拟长按,我们需要 sdk 里的几个 jar 包,它们分别是(以下版本省略为*):

  • chimpchat-*.jar
  • common-*.jar
  • ddmlib-*.jar
  • guava-*.jar

以上这些 jar 包在 sdk 中的 tools/lib/ 中可以找到。然后我们使用其 API 来实现 ADB 连接,截图,长按。代码如下:

package com.githang.autojump;

import com.android.chimpchat.adb.AdbBackend;
import com.android.chimpchat.core.IChimpDevice;
import com.android.chimpchat.core.IChimpImage;

import java.awt.image.BufferedImage;

/**
 * @author 黄浩杭 (huanghaohang@parkingwang.com)
 */
public class AdbHelper {
    private final AdbBackend mAdbBackend = new AdbBackend();
    private IChimpDevice mChimpDevice;

    public void waitForConnection() {
        mChimpDevice = mAdbBackend.waitForConnection();
    }

    public void disconnect() {
        mChimpDevice.dispose();
    }

    /**
     * 截图
     */
    public BufferedImage snapshot() {
        IChimpImage img;
        // 当尝试次数太多时不再尝试。
        int tryTimes = 0;
        do {
            System.out.println("截图中.." + tryTimes);
            img = mChimpDevice.takeSnapshot();
            tryTimes++;
        } while (img == null && tryTimes < 15);
        if (img == null) {
            throw new RuntimeException("try to much times to take snapshot but failed");
        }
        return img.getBufferedImage();
    }

    /**
     * 长按
     * @param x x坐标
     * @param y y坐标
     * @param ms 长按时间,单位毫秒
     */
    public void press(int x, int y, int ms) {
        mChimpDevice.drag(x, y, x, y, 1, ms);
    }
}

上面的代码中,由于 IChimpDevice 没有直接提供长按的接口,这里使用的是拖拽方法。

找到起始位置

接下来是要找到跳跃的起始位置。这里我们简化一下,允许存在一些误差,找到小球的中心的 x 坐标,和目的点的 x 坐标即可。
由于在每一步中小球都不会有变化,并且在小球上面不会有其他色块与小球颜色接近,所以我想到的找到小球中心的 x 坐标的思路如下:
先获取小球的一部分图像,然后截图整个界面,由上至下遍历,找到与小球颜色接近的那块区域,其中心点就是小球的中心点。
截图小球的方式有多种,比如用 PS 抠图,或 QQ 截图,代码也可以,方式如下:
先截取游戏时的界面,然后用 PxCook 测量出小球的位置。我手机屏幕为720 * 1280,截取的是小球中间的 24 * 24的一块区域,代码如下:

    private static void clipTarget() throws IOException {
        BufferedImage image = ImageIO.read(new File("screen.png"));
        BufferedImage target = new BufferedImage(24, 24, BufferedImage.TYPE_INT_ARGB);
        for (int i = 0; i < 24; i++) {
            for (int j = 0; j < 24; j++) {
                target.setRGB(i, j, image.getRGB(212 + i, 620 + j));
            }
        }
        ImageIO.write(target, "png", new File("target.png"));
    }

上面的 212620 是在 PxCook 中测量出来的数值。

然后就是找出每一步小球所在的位置。思路也很简单,逐行往下遍历,如果某个像素点的颜色与刚才所获取的小球区域的颜色相近,则遍历这个点为起始点的[x,y][x+23, y+23]矩形区域,如果每个像素点都接近,则表示这块区域就是小球,否则则继续遍历。判断颜色相近,我想到的是其 R G B 相差均小于10。最终找到小球的代码如下:

    private static boolean isJumpFrom(BufferedImage source, BufferedImage target, int x, int y) {
        for (int i = 0, width = target.getWidth(); i < width; i++) {
            for (int j = 0, height = target.getHeight(); j < height; j++) {
                int colorValue = target.getRGB(i, j);
                if (colorValue == 0) {
                    continue;
                }
                int tempX = x + i;
                int tempY = y + j;
                Color targetColor = new Color(colorValue);
                Color sourceColor = new Color(source.getRGB(tempX, tempY));
                if (Math.abs(targetColor.getRed() - sourceColor.getRed()) > 10
                        || Math.abs(targetColor.getGreen() - sourceColor.getGreen()) > 10
                        || Math.abs(targetColor.getBlue() - sourceColor.getBlue()) > 10) {
                    return false;
                }
            }
        }
        return true;
    }

从上往下逐行进行遍历,代码如下:

int width;
int height;
BufferedImage target = ImageIO.read(new File("target.png"));
int bWidth = target.getWidth();
int bHeight = target.getHeight();
while (true) {
    BufferedImage image = helper.snapshot();
    width = image.getWidth();
    height = image.getHeight();
    LOG.info("查找位置");
    int toX = findToX(width, height, image);
    FINDING:
    for (int y = height * 2 / 5, endY = height * 4 / 5; y < endY; y++) {
        for (int x = 0, endX = width - bWidth; x < endX; x++) {
            if (isJumpFrom(image, target, x, y)) {
                final int targetX = x + bWidth / 2;
                final int targetY = y + bHeight / 2;
                LOG.info("找到位置: " + targetX + ", " + targetY);
                final int distance = Math.abs(toX - targetX);
                final int time;
                // TODO 计算出时间
                helper.press(targetX, targetY, time);
                Thread.sleep(time);
                break FINDING;
            }
        }
    }
}

找出跳跃终点

在上面的代码中,有个 findToX(width, height, image) 方法的调用,它就是用于找出跳跃的终点坐标。它的原理也很简单。
由于在游戏中,背影是纯色渐变的,而要到达的区域,它的顶面颜色与背影色相差较大,并且不是椭圆就是菱形,它的特点是在这个平面上,中心点与顶点的 x 坐标相同。所以只要在显示分数的下面,从上往下每一行进行遍历,找到有一点与上面那一行的点颜色相差较大,这个点的 x 坐标就是要跳过去的点的 x 坐标了。代码如下:

    private static int findToX(int width, int height, BufferedImage image) {
        for (int y = height / 5, endY =  height / 2; y < endY; y++) {
            Color background = new Color(image.getRGB(2, y - 1));
            for ( int x = 0; x < width; x++) {
                Color color = new Color(image.getRGB(x, y));
                if (Math.abs(color.getRed() - background.getRed()) > 10
                        || Math.abs(color.getGreen() - background.getGreen()) > 10
                        || Math.abs(color.getBlue() - background.getBlue()) > 10) {
                    LOG.info("跳到:" + x + ", " + y);
                    return x;
                }
            }
        }
        throw new RuntimeException("ToX not found!");
    }

最终代码

最后是写一个大循环,每一步里面截图,找到要终点 x 坐标,找到小球中心,算出距离,再换算成时间,发送模拟按下事件,然后暂停按下的时间,再暂停 2 秒。这里暂停 2 秒的原因是,有些场景,停留 2 秒以上会有加分。最终 main 方法代码如下:

    public static void main(String[] args) throws InterruptedException, IOException {
        AdbHelper helper = new AdbHelper();
        helper.waitForConnection();
        int width;
        int height;
        BufferedImage target = ImageIO.read(new File("target.png"));
        int bWidth = target.getWidth();
        int bHeight = target.getHeight();
        while (true) {
            BufferedImage image = helper.snapshot();
            width = image.getWidth();
            height = image.getHeight();
            LOG.info("查找位置");
            int toX = findToX(width, height, image);
            FINDING:
            for (int y = height * 2 / 5, endY = height * 4 / 5; y < endY; y++) {
                for (int x = 0, endX = width - bWidth; x < endX; x++) {
                    if (isJumpFrom(image, target, x, y)) {
                        final int targetX = x + bWidth / 2;
                        final int targetY = y + bHeight / 2;
                        LOG.info("找到位置: " + targetX + ", " + targetY);
                        final int distance = Math.abs(toX - targetX);
                        final int time;
                        time = Math.max(330, (int) (distance * 2.38f));
                        LOG.info("距离:" + distance + "  按下时间:" + time + "ms");
                        helper.press(targetX, targetY, time);
                        Thread.sleep(time);
                        break FINDING;
                    }
                }
            }
            Thread.sleep(2000);
        }
    }

其中由距离计算出时间的公式可以自己再进行调优。目前我的公式基本都能拿三四百分以上,最高是 800 多分。

项目完整代码,请参见 Github 项目:https://github.com/GameTerminator/AutoJump。Idea Gradle 项目。
其他相关项目:

排行榜:
排行榜
在朋友里刷个第一名还是挺简单的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值