用Node.js写一个跳一跳外挂
Posted 前端那些事儿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了用Node.js写一个跳一跳外挂相关的知识,希望对你有一定的参考价值。
思路
对游戏画面截图
识别图像,确定当前位置和目标点位置
根据距离计算出跳的时间
进行模拟操作
实现
1. 截图
android中内置了截图的screencap
命令,可以通过adb shell
进行调用。
为了方便调用,先对adb做个封装
const ADB_PATH = '/PATH_OF_ANDROID_SDK/platform-tools/adb';async function adb(shell, ...args) {
console.log(args.join(' '));
return await new Promise((resolve, reject) => {
const process = child_process.spawn(ADB_PATH, [shell, ...args]);
const outBufs = [];
process.stdout.on('data', data => {
outBufs.push(data);
});
process.on('close', code => {
if (code === 0) {
resolve(Buffer.concat(outBufs));
} else {
reject(`Error: ${code}`);
}
});
});}
截图就直接调用screencap
就行了,添加-p
的参数表示以png格式输出
async function screenshot() {
return await adb('shell', 'screencap', '-p');}
2. 图像识别
图像识别的目的是找出小人和目标方块的位置,以便计算距离。为了简单,我用了一种比较“偷懒”的方法
从图中可以看出,我们要计算的实际距离是红色线段的长度,由于游戏视角度固定,所以红色线的斜率几乎是不变的(由于小人站的位置并不固定,或左或右,所以角度其实会有微小的变化,不过为简单起见先忽略),所以红色和蓝色线段的比例固定,继而可以简化成计算蓝色线段的长度,即在水平方向上目标盒子中心点和小人中心点的距离。
找中点就比较简单了,只要去掉顶部分数的部分,画面的最高点就是目标盒子的水平方向上的中点。找小人也很容易,小人的头部为60x60(分辨率1920x1080下)的圆形,那么只要根据长宽或者面积筛选出这个轮廓,然后找到轮廓的最高点即可。两个最高点的X坐标相减求绝对值就是要求距离了。
图像识别使用了OpenCV库的Node.js封装node-opencv,功能很强大,只可惜文档写的很差,很多方法都是扒源码和examples找到的。
首先获取截图,然后用OpenCV打开图片:
const sc = await screenshot();
const img = await wrapper(cv.readImage, cv, sc);
获取目标盒子中点的X坐标:
function getTargetX(img) {
img = img.clone();
// 转化为灰度图,用于边缘检测
img.cvtColor('CV_BGR2GRAY');
// 使用canny算法进行边缘检测
const lowThresh = 2;
const highThresh = 100;
img.canny(lowThresh, highThresh);
// 获取所有轮廓线
const contours = img.findContours();
const opt = {
filter(i) {
// 有时两个盒子离得特别近,这时小人的头会高过盒子的顶点
// 简单粗暴,通过面积排除掉小人的头
const area = contours.area(i);
return area < 2500 || 2900 < area;
}
};
// 计算顶部点X坐标的平均值
return getTopPointsAvgX(contours, 400, opt);}
获取小人中点的X坐标也类似:
function getCurrentX(img, id) {
img = img.clone();
// 这里很奇怪,如果不这么转一次的话后面得不到正确的过滤结果
img.cvtColor('CV_BGR2Lab');
img.cvtColor('CV_Lab2BGR');
// 根据颜色过滤出小人
img.inRange([50, 50, 50], [120, 80, 80]);
// 获取轮廓线
const contours = img.findContours();
const opt = {
filter(i) {
// 根据面积,过滤出小人的头
const area = contours.area(i);
return 2500 < area && area < 2900;
}
};
// 计算顶部点X坐标的平均值
return getTopPointsAvgX(contours, 400, opt);}
这两个方法中都用到了getTopPointsAvgX
,这是封装的一个公共方法,作用是计算所有轮廓中最高的一个轮廓中顶点的X坐标(为了精确这里计算了所有最高点X坐标的平均值):
function getTopPointsAvgX(contours, offsetY = 0, opt = {}) {
// 取出最高的物体,即为目标
let minTopContour = Infinity;
let targetIndex = -1;
for (let i=0; i<contours.size(); i++) {
const rect = contours.boundingRect(i);
// 排除距离顶部小于offsetY的物体,例如分数、菜单等附加信息
// 根据自定义的opt.filter排除其他干扰物
if (rect.y < offsetY || (opt.filter && !opt.filter(i))) {
continue;
}
if (rect.y < minTopContour) {
minTopContour = rect.y;
targetIndex = i;
}
}
// 获取最高物体中的最高点
const minY = Math.min(...contours.points(targetIndex).map(p => p.y));
// 取出所有顶部的点
const topPoints = contours.points(targetIndex).filter(p => p.y === minY);
// 计算顶部点X坐标的平均值
const averageX = topPoints.reduce((sum, a) => sum + a.x, 0) / topPoints.length;
return averageX;}
把这些轮廓画下来就是这个样子
计算距离就很简单了:
const currentPointX = getCurrentX(img);
const targetPointX = getTargetX(img);
const length = Math.abs(targetPointX - currentPointX);
3. 计算时间
为了求出小人起跳时长和跳跃距离的关系,首先根据经验设定一些时间值进行试跳,然后用PS计算出试跳的长度,记录下这些值
时长(MS) | 距离(PX) |
---|---|
913 | 556 |
363 | 194 |
426 | 216 |
780 | 445 |
635 | 372 |
导入到Numbers中画出散点图,并进行拟合
得到拟合结果:t = 1.5089x + 85.333
把上一步求出的距离带入即可:
const second = Math.round(1.5089 * length + 85.333);
4. 跳
Android内置的input
命令提供了对触控和键盘输入的模拟的功能。一般来说,屏幕点击可以通过input touch x y
进行模拟,但是这个命令并不能模拟长按,不过我们可以通过另外一个命令input swipe x0 y0 x1 y1 [duration]
模拟长按。
顺便提一下,MIUI系统中对“模拟输入”这种危险性较高的命定额外加了一层限制,需要打开“开发者选项”中的“USB调试(安全设置)”才能使用。
顺手加了几个随机数,防止被封:
async function jump(ms) {
const touchX1 = Math.round(Math.random() * 740 + 100);
const touchY1 = Math.round(Math.random() * 740 + 220);
const touchX2 = Math.round(Math.random() * 20 + touchX1);
const touchY2 = Math.round(Math.random() * 20 + touchY1);
await adb('shell', 'input', 'swipe', touchX1, touchY1, touchX2, touchY2, Math.round(ms));}
花了一下午写了这个小东西,试了一下打出了840分,最后一次对目标盒子边缘识别错了,导致直接飞了出去。
代码中还有好多不足:
边缘识别偶尔会出错,参数还需要微调;
opencv识别的图像次数多了之后有时轮廓识别混杂前几次的结果,原因不明,感觉不在js这儿;
文中提过,小人的位置计算并不精确;
如果需要把外挂做成自动连跳,则需要对下水道、超市、魔方含有彩蛋的方块进行识别;
...
养病去了,不优化了,写出来抛砖引玉。
以上是关于用Node.js写一个跳一跳外挂的主要内容,如果未能解决你的问题,请参考以下文章