前一段时间微信小游戏跳一跳很火,那时候也玩了,但是一直没法突破高分,所以就想能不能写一个脚本自动操作。
在这个想法之前,曾想过要做一个脚本,用于破解某一家公司的微信游戏(跟别踩白块的玩法一样)刷分。但是不能同时具备跨应用和快速度,最后失败了,但是这次的实践,积累了图片处理和截屏方面的技术知识,在跳一跳的游戏脚本制作上就有很好的发挥空间。
在内容开始的之前,本人在此声明,MediaProjection部分是借助别人的代码,但是图片技术处理是利用自己的技术储备。同时由于个人兴趣所做,效果可以刷分但是防作弊做的不好,容易被检测到作弊嫌疑,因此本博文只能作为技术交流,不能作为真正的刷分之用。有兴趣的可以考虑解决防作弊方案。
首先、我们对MediaProjection进行简要的说明,MediaProjection是Android 5.0 之后开放的屏幕截屏和屏幕录制功能,在使用的时候,只要检测手机是否有授权此功能,然后授权即可(详细的可以查看别人的分析)。得到授权之后,我们设置悬浮框,打开微信的跳一跳小游戏,点击悬浮框,截屏并获取该图的资源(图片处理的原始数据)。原图(分辨率:1920*1080)如下:
图1 原图
获取图片资源-->将图片压缩(取样)-->图片灰度化(作为下一处理过程的原数据)
在这个步骤中我们获取每3*3获取一个样本点。压缩之后的图片宽度和高度均为原来的1/3,可以大大减少图片的处理速度。压缩完的图片分辨率还是足以满足后续的扣模型和寻找着地点位置坐标的需求。压缩代码如下,压缩结果如下(右边图,点击查看大图可以看出有失真):
/**
* 将图片的大小缩小为原来的1/9(九个样本点取一点)
* @param pixels 缩小之后的图片
* @param oldPixels 原图片的数据
* @param width 缩小之后的图片的宽度
* @param height 缩小之后的图片的高度
*/
public void narrow(int[] pixels,int[] oldPixels,int width,int height){
for(int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
pixels[i*width + j] = oldPixels[i*width*9 + j*3];
}
}
}
图2 压缩图
该步骤需要先将图片的色码按红绿蓝的比重灰度化(我这边的red:greean:blue比重是:3 :6:1 个人实际操作是3:4:3),图片灰度化后效果与原图的对比如下图所示。
/**
* 将图片灰度化
* @param pixels 图片压缩后待处理的数据
* @param width 图片的宽度
* @param height 图片的高度
*/
public int[] convertNormalGrey(int[] pixels,int width ,int height){
int[] greyPixels = new int[pixels.length];
for(int i = 0; i < height; i++) {
for(int j = 0; j < width; j++) {
int red,green,blue,gray;
if(i < 500 && i > 200){
int grey = pixels[width * i + j];
red = (grey >> 16) & 0xff;
green = (grey >> 8) & 0xff;
blue = grey & 0xff;
//在这里采用的是灰度化处理比重是0.3、0.6、0.1,这个可以根据自己的理解或
//者是调试过程中发现的问题的处理而修改
gray = (int)((float) red * 0.3 + (float)green * 0.6 + (float)blue * 0.1);
} else {
gray = 255;
}
greyPixels[width * i + j] = gray;
}
}
return greyPixels;
}
图3 灰度化图
灰度化图片-->黑白化图片-->获取模型并保存-->分析模型并选取判断点-->弹跳前截图判断-->得出弹跳前的起跳点
取定一个分界值(模型:85 定位:100),将灰度化后的图片数据极化成0/1(减少内存的占用和提高计算速度),其中1代表着浅色,0代表值深色(在显示的时候0用黑色色码:#000000 1用白色色码:#FFFFFF(16777215)代替,结果就是图片二值化处理结果),黑白化图片结果如下图左图所示。根据处理结果显示,我们可以截取模型区域的数据,如下图中间图所示。选取的模型样本点位置如下图右边图的红点标识所示。
/**
* 图片黑白化,在这里处理之后图片的色码只有黑色#000000和白色#FFFFFF,
* 这里用0代表#000000,1代表#FFFFFF(减少计算量和内存占用)
* @param pixels 图片灰度化后待处理的数据
* @param width 图片的宽度
* @param height 图片的高度
*/
public int[] toBlackAndWhite(int[] pixels,int width ,int height){
int[] blackWhilePixels = new int[pixels.length];
for(int i = 0; i < height; i++) {
for(int j = 0; j < width; j++) {
int grey;
//因为在页面中的上下部分的很大一块区域是不会影响到事件的处理的
//压缩后的图片的高度是640,这里只取值200-500间的数据处理,其他的
// 全部设置为浅色(白色),为的是减少计算量
if(i < 500 && i > 200){
//标准值如果是获取模型的我们去85 如果是判断模型位置的时候,用的是100
//判断色码的标准值是100,大于100的归类为浅色(白色),小于100归类为深色(黑色)
if(pixels[width * i + j] < 85){
grey = 0;
} else {
grey = 1;
}
} else {
grey = 1;
}
blackWhilePixels[width * i + j] = grey;
}
}
return blackWhilePixels;
}
在每次的弹跳前截取图片,将图片以上述的方法灰度化之之后,再将图片黑白化(此时的黑白化的判断值位100),得到的黑白化的图片如下图所示,扫描黑白化图片200-500高度之间区域,如果获取到第一个位置点(该样本点是连续的6个黑点),则同时判断其他的10个样本点,根据所有位置的判断结束之后,如果匹配率都达到80%说明找到了模型(我这边是判断所有位置点小于20个点匹配不上即可),同时也可以确定模型的中心点,这样子就可以确定起跳点了。如果扫描完之后还是没有找到模型,可能的原因是截图获取的图片的数据不完整(概率最大)或者是页面中的方块颜色过于相近而无法识别(概率极低,此时需要针对性的优化程序 比如:调整黑白化判断值和red:green:blue的比重)
/**
* 获取模型的位置,先通过查找第一个样本点的位置(连续6个黑点即当做获取到第一个样本点)
* 然后再判断其他10个样本点的数据(所有样本点总和有200个位置点)
* @param pixels 原始数据
* @param width 原始数据的宽度
* @param height 原始数据的高度
* @return 如果查到模型,就返true,否则返回false
*/
public boolean analyModel(int[] pixels,int width,int height){
//记录下当前连续黑点的几个
int blackNum = 0;
fromX = fromY = 0;
for(int i = 0; i < height; i++) {
for(int j = 0; j < width; j++) {
if(pixels[width * i + j] == 0) {
if(blackNum++ >= 6){
//获取所有样本点上没有匹配上的位置点的数量(具体方法太长这里不显示,详情查看源码)
int count = ModelAlgorithmUtils.getSampleCount(pixels,width,j,i);
if(count < 20){
//该处的的值就是起跳点的位置点
fromX = j - 3;
fromY = i + 61;
return true;
}
}
} else {
blackNum = 0;
}
}
}
return false;
}
图7 图片黑白化
灰度化图 --> 图片边界化 --> 根据规则(通过分析游戏原理得到的)获取着地点坐标
根据图7 黑白化图显示,分析发现,如果在获取着地点的时候也是采用图片的黑白化来处理的话,很容易出现会将着地点的物体浅化了。所以在获取着地点时,我们采用的是图片的边沿化处理技术。简单来说,如果图片区域内颜色没有变化,或者说是变化很小,那我们就将该区域浅化,否则深度化。为了防止起跳点和着地点很近导致小人的高度干扰到着地点的判断,一般情况下在获取着地点的时候,将小人去除,结果如下图所示。左图为不去小人 右图为去掉小人。
/**
* 将图片边界化 在边界化中,有时候下一个弹跳点距离太近,所以在边界化的时候,防止距离太近
* 影响到在边界化的时候,要将小人点的浅化
* @param pixels 图片的源数据
* @param width 图片的宽度
* @param height 图片的高度
*/
public int[] toBorder(int[] pixels,int width ,int height){
int[] borderPixels = new int[pixels.length];
//把小人的中心点的前后25个像素点的边界清空
int right = fromX + 25;
int left = fromX - 25;
for(int i = 0; i < height; i++) {
for(int j = 0; j < width; j++) {
int grey;
if(j > 10 && j < width - 10) {
if(i < 500 && i > 200){
int preTemp = pixels[width * i + j - 1];
int temp = pixels[width * i + j];
int nextTemp = pixels[width * i + j + 1];
//不去除小人的边界化处理
// grey = (Math.abs(temp - preTemp) > 10 || Math.abs(temp - nextTemp) > 10)?0:1;
//把小人的中心点的前后25个像素点的边界删除掉,防止小人点的太高影响到下一阶的判断
if(j < right && j > left){
grey = 1;
} else {
grey = ( Math.abs(temp - preTemp) > 10 || Math.abs(temp - nextTemp) > 10)?0:1;
}
} else {
grey = 1;
}
} else {
grey = 1;
}
borderPixels[width * i + j] = grey;
}
}
return borderPixels;
}
图8 没有去除小人的边沿化 图9 去除小人的边沿化
通过大量的研究和分析,我们发现了在该游戏中,小人站的物体都是左右对称的(个别不对称,但是不影响到结果),所以我们只要获取到了着地点的物体的最顶端的点的位置就相当于获取到了小人的着地点的X坐标。在边沿化图上可以看到,整张图片的最顶端的黑点其实就是着地点的物体的顶点,所以在确定获顶点位置之后,我们获取顶点下面一段距离的横线上所有黑点的X轴的平均值(主要是为了提高精度),代码处理如下:
/**
* 获取小人着地点的X坐标,在获取到最顶端的黑点之后,获取该点下面一段距离的
* 一行黑点的X坐标值的平均值作为着地点
* @param pixels 图片的源数据
* @param width 图片的宽度
* @param height 图片的高度
*/
public int getBorderTopY(int[] pixels,int width ,int height){
for(int i = 0; i < height; i++) {
for(int j = 0; j < width; j++) {
if(i < 500 && i > 200){
//如果检测到第一个点的位置,则目的地的点的X轴坐标等于往下移动两格的所有点的平均值
if(pixels[width * i + j] == 0){
int count = 0,result = 0;
for(int z = 0; z < width; z++){
if(pixels[width *( i + 2) + z] == 0){
result += z;
count++;
}
}
return count == 0?0:result/count;
}
}
}
}
return 0;
}
至此,已经获取到了小人的起跳点和着地点位置。接下去只要整理了每一次的长按时间与距离之间的关系就可以很好地实现操作了。细致的调试中,我们发现了跳跃长度跟长按时间是成正比的。所以我们通过确定着地点和起跳点的距离,就可以获取线性参数。
长按事件的原理
长按事件我们是通过执行shell命令来实现,在实现该命令之前需要事先获取root权限,然后再把计算好的时间传入即可。
注意:在这个脚本中,我的参数都是根据1920*1080的屏幕手机来设定的,如果是其他分辨率的手机,需要重新测定来实现(主要是太懒,不想去做一个匹配所有机型的),关于防止被检测到防止作弊的处理上,我有尝试过几种方法,但是只要分数高都是无法避免被检测到,这部分本人不再处理,有兴趣的可以自己想办法解决。
脚本源码:http://download.csdn.net/download/huadashihuangzhe/10255375