补发30日未发的博客。
PS:最新改进的算法和针对我自己手机调优的参数已经停不下来了,见项目:https://github.com/GameTerminator/AutoJump
几年前参考网上的文章写过天天连萌自动玩的项目(之前写在 iteye 的博客上:http://maosidiaoxian.iteye.com,github 项目地址为:https://github.com/GameTerminator/lianmeng),这次微信小游戏里的跳一跳玩了玩,就自然而然地想到用同样的方式来做。
经过几次修正和简化,最终思路和实现如下:
- 使用 monkeyrunner 里的接口截图
- 找出跳动的那个小球的 x 坐标
- 找出最终要跳达的点的 x 坐标
- 算出其距离,并按线性方程计算出时间
- 使用 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"));
}
上面的 212
及 620
是在 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 项目。
其他相关项目:
- 天天连萌自动玩:https://github.com/GameTerminator/lianmeng
- 别踩白块儿暴力玩法:https://github.com/GameTerminator/dont-touch-white
排行榜:
在朋友里刷个第一名还是挺简单的。