其實(shí)也不能說(shuō)算是外掛吧,算是個(gè)游戲小助手吧,畢竟不能抓包,也不能直接修改分?jǐn)?shù)(據(jù)說(shuō)之前可以直接抓包修改分?jǐn)?shù),不過(guò)這漏洞已經(jīng)被微信官方修復(fù)),今天這個(gè)是 Android 同學(xué)可以非常容易看懂的一篇文章,是從 Android 的角度實(shí)現(xiàn)的,附帶著技術(shù)原理分析和代碼分析。
這個(gè)開(kāi)源庫(kù)已經(jīng)被我同學(xué)分享到 GitHub 上,他自己很無(wú)聊,就寫(xiě)了這個(gè)東西和這篇文章,自己通過(guò)寫(xiě)代碼實(shí)現(xiàn)高分也是玩的不亦樂(lè)乎,這就是程序員和普通玩家的區(qū)別吧。
開(kāi)源庫(kù)地址:https://github.com/xushanmeng/WechatJumpHelper
功能簡(jiǎn)介
用JAVA自動(dòng)控制手機(jī)玩跳一跳
自動(dòng)識(shí)別圖像計(jì)算距離
自動(dòng)幫你點(diǎn)擊屏幕
-
自動(dòng)緩存圖片,并在圖片上標(biāo)記一些識(shí)別結(jié)果,如下圖:
image
運(yùn)行環(huán)境
JAVA,最低版本為7.0,官網(wǎng)下載
adb驅(qū)動(dòng),官網(wǎng)下載(需要翻墻),或者到這里下載SDK-tools,其中就包含adb
安卓手機(jī),目前已適配分辨率
1600x2560
1440x2560
1080x1920
720x1080
使用方法
有JAVA開(kāi)發(fā)工具的同學(xué)可以直接運(yùn)行java代碼,便于代碼調(diào)試,下面主要介紹運(yùn)行已經(jīng)打包好的jar包的方法
- 手機(jī)打開(kāi)USB調(diào)試,并連接電腦
打開(kāi)USB調(diào)試方法,進(jìn)入
設(shè)置
,找到開(kāi)發(fā)者選項(xiàng)
,打開(kāi)并勾選USB調(diào)試
;如果沒(méi)有
開(kāi)發(fā)者選項(xiàng)
,進(jìn)入關(guān)于手機(jī)
,連續(xù)點(diǎn)擊版本號(hào)
7次,即可開(kāi)啟開(kāi)發(fā)者選項(xiàng)
。
-
通過(guò)下面的命令,運(yùn)行Android.jar
java -jar Android.jar
-
根據(jù)手機(jī)分辨率選擇跳躍系數(shù),目前已適配機(jī)型:
其他分辨率請(qǐng)自己微調(diào)。
1600x2560機(jī)型推薦0.92
1440x2560機(jī)型推薦1.039
1080x1920機(jī)型推薦1.392
720x1080機(jī)型推薦2.078
原理說(shuō)明
-
通過(guò)adb命令控制手機(jī)截圖,并取回到本地
adb shell screencap -p /sdcard/screen.png
adb pull /sdcard/screen.png .
圖片分析
有靶點(diǎn),即目標(biāo)物體中心的白色圓點(diǎn),則靶點(diǎn)中心為目標(biāo)落點(diǎn)
無(wú)靶點(diǎn),但是純色平面,或者規(guī)則平面,則平面中心為目標(biāo)落點(diǎn)
無(wú)靶點(diǎn),又無(wú)純色規(guī)則平面,但是左上和右上位置的斜率是固定的,可根據(jù)固定斜率的斜線(xiàn)和目標(biāo)物體中心線(xiàn)的焦點(diǎn)計(jì)算落點(diǎn)
根據(jù)棋子的顏色,取頂部和底部的特征像素點(diǎn),在截圖中進(jìn)行匹配,找到棋子坐標(biāo)
-
由于目標(biāo)物體不是在左上就是在右上,可以從上往下掃描,根據(jù)色差判斷目標(biāo)物體位置,其中又分為以下幾種類(lèi)型
imageimageimage 計(jì)算棋子坐標(biāo)和目標(biāo)落點(diǎn)的距離
距離×跳躍系數(shù)=按壓屏幕的時(shí)間,不同分辨率的手機(jī),跳躍系數(shù)也有所不同
-
通過(guò)adb命令,給手機(jī)模擬按壓事件
adb shell input swipe x y x y time
其中
x
和y
是屏幕坐標(biāo),time
是觸摸時(shí)間,單位ms。
工程結(jié)構(gòu)
代碼詳解
這里將針對(duì)一些關(guān)鍵算法的代碼進(jìn)行解釋
-
尋找棋子位置
把截圖放大,可以看到棋子頂部像素連成一條橫線(xiàn),那么我們通過(guò)顏色匹配,找到這一條線(xiàn)的始末位置,取中間位置,就得到了棋子的x坐標(biāo)。
image棋子的底部也是一條橫線(xiàn),用顏色匹配,我們檢測(cè)到相似顏色的最大y坐標(biāo),就是棋子底部了,不過(guò)考慮到棋子底部是個(gè)圓盤(pán),我們把棋子的y坐標(biāo)再往上提一些。
image.gif這樣我們就得到了棋子的xy坐標(biāo),下面是相關(guān)代碼:
/* 計(jì)算棋子位置 */Pixel piece = new Pixel();for (int i = TOP_BORDER; i < screenHeight - BOTTOM_BORDER; i++) { int startX = 0; int endX = 0; for (int j = LEFT_BORDER; j < screenWidth - RIGHT_BORDER; j++) { int red = Color.red(pixels[i][j].color); int green = Color.green(pixels[i][j].color); int blue = Color.blue(pixels[i][j].color); if (50 < red && red < 55 && 50 < green && green < 55 && 55 < blue && blue < 65) {//棋子頂部顏色 //如果偵測(cè)到棋子相似顏色,記錄下開(kāi)始點(diǎn) if (startX == 0) { startX = j; endX = 0; } } else if (endX == 0) { //記錄下結(jié)束點(diǎn) endX = j; if (endX - startX < PIECE_TOP_PIXELS) { //規(guī)避井蓋的BUG,像素點(diǎn)不夠長(zhǎng),則重新計(jì)算 startX = 0; endX = 0; } } if (50 < red && red < 60 && 55 < green && green < 65 && 95 < blue && blue < 105) {//棋子底部的顏色 //最后探測(cè)到的顏色就是棋子的底部像素 piece.y = i; } } if (startX != 0 && piece.x == 0) { piece.x = (startX + endX) / 2; }}//棋子縱坐標(biāo)從底部邊緣調(diào)整到底部中心piece.y -= PIECE_BOTTOM_CENTER_SHIFT;
-
尋找靶點(diǎn)
所謂靶點(diǎn),就是目標(biāo)物體中心的那個(gè)小圓點(diǎn),顏色值為
0xf5f5f5
。image那么我們只需要尋找顏色值為0xf5f5f5的色塊就可以了,為了規(guī)避其他物體相近顏色干擾,我們可以限制色塊的大小,正確大小的色塊才是靶點(diǎn)。
但是如何計(jì)算色塊的大小呢,色塊最頂端到最底端y坐標(biāo)的差值我們作為色塊的高度,同理,最左側(cè)到最右側(cè)x坐標(biāo)的差值作為寬度,我們只需要查找這四個(gè)頂點(diǎn)的坐標(biāo)就可以了。
本來(lái)打算用凸包的Graham掃描算法,后來(lái)發(fā)現(xiàn)色塊已經(jīng)是凸包了,且邊緣像素是連續(xù)的,那么我們按照一定順序,遍歷邊緣像素,就可以在O(n^-2)的時(shí)間復(fù)雜度里,得到色塊的頂點(diǎn)坐標(biāo)了。
我們從第一個(gè)像素點(diǎn)開(kāi)始,尋找的順序如圖所示:
image/** * 尋找色塊頂點(diǎn)像素 */ public static final Pixel[] findVertexs(Pixel[][] pixels, Pixel firstPixcel) { Pixel[] vertexs = new Pixel[4]; Pixel topPixel = firstPixcel; Pixel leftPixel = firstPixcel; Pixel rightPixel = firstPixcel; Pixel bottomPixel = firstPixcel; Pixel currentPixcel = firstPixcel; //先把坐標(biāo)置于左上角 while (checkBorder(pixels, currentPixcel)//判斷是否超出圖像邊緣 && Color.compareColor(pixels[currentPixcel.y - 1][currentPixcel.x], firstPixcel)) {//判斷是否是相同顏色 currentPixcel = pixels[currentPixcel.y - 1][currentPixcel.x]; } while (checkBorder(pixels, currentPixcel) && Color.compareColor(pixels[currentPixcel.y][currentPixcel.x - 1], firstPixcel)) { currentPixcel = pixels[currentPixcel.y][currentPixcel.x - 1]; } //尋找上頂點(diǎn)像素 while (checkBorder(pixels, currentPixcel)) { if (Color.compareColor(pixels[currentPixcel.y - 1][currentPixcel.x], firstPixcel)) { currentPixcel = pixels[currentPixcel.y - 1][currentPixcel.x]; } else if (Color.compareColor(pixels[currentPixcel.y][currentPixcel.x + 1], firstPixcel)) { currentPixcel = pixels[currentPixcel.y][currentPixcel.x + 1]; } else { topPixel = findCenterPixcelHorizontal(pixels, currentPixcel); break; } } //尋找右頂點(diǎn)像素 while (checkBorder(pixels, currentPixcel)) { if (Color.compareColor(pixels[currentPixcel.y][currentPixcel.x + 1], firstPixcel)) { currentPixcel = pixels[currentPixcel.y][currentPixcel.x + 1]; } else if (Color.compareColor(pixels[currentPixcel.y + 1][currentPixcel.x], firstPixcel)) { currentPixcel = pixels[currentPixcel.y + 1][currentPixcel.x]; } else { rightPixel = findCenterPixcelVertial(pixels, currentPixcel); break; } } //尋找下頂點(diǎn)像素 while (checkBorder(pixels, currentPixcel)) { if (Color.compareColor(pixels[currentPixcel.y + 1][currentPixcel.x], firstPixcel)) { currentPixcel = pixels[currentPixcel.y + 1][currentPixcel.x]; } else if (Color.compareColor(pixels[currentPixcel.y][currentPixcel.x - 1], firstPixcel)) { currentPixcel = pixels[currentPixcel.y][currentPixcel.x - 1]; } else { bottomPixel = findCenterPixcelHorizontal(pixels, currentPixcel); break; } } //尋找左頂點(diǎn)像素 while (checkBorder(pixels, currentPixcel)) { if (Color.compareColor(pixels[currentPixcel.y][currentPixcel.x - 1], firstPixcel)) { currentPixcel = pixels[currentPixcel.y][currentPixcel.x - 1]; } else if (Color.compareColor(pixels[currentPixcel.y - 1][currentPixcel.x], firstPixcel)) { currentPixcel = pixels[currentPixcel.y - 1][currentPixcel.x]; } else { leftPixel = findCenterPixcelVertial(pixels, currentPixcel); break; } } vertexs[0] = leftPixel; vertexs[1] = topPixel; vertexs[2] = rightPixel; vertexs[3] = bottomPixel; return vertexs; }
得到了四個(gè)坐標(biāo)點(diǎn),我們就可以計(jì)算色塊的中點(diǎn)了,也就是目標(biāo)落點(diǎn)。
對(duì)于沒(méi)有靶點(diǎn),但是落點(diǎn)是規(guī)則平面的,也可以用類(lèi)似算法。
-
斜率計(jì)算對(duì)于沒(méi)有靶點(diǎn),又不是規(guī)則平面的,我們?cè)趺从?jì)算落點(diǎn)呢,這時(shí)候就要用到斜率了。
可以看得出來(lái),每次左上角或右上角出現(xiàn)的物體,針對(duì)當(dāng)前物體的方向都是一樣的,也就是兩個(gè)物體中心的連線(xiàn),斜率是固定的。
基本所有的目標(biāo)物體,最頂點(diǎn)像素中點(diǎn)的x坐標(biāo),都是在物體中間,我們至少先得到了目標(biāo)物體x坐標(biāo)了,記為des.x ,接下來(lái)要求des.y 。
image.gif如上圖所示,計(jì)算過(guò)程如下:
斜線(xiàn)的公式為 y=kx+b
那么,在棋子坐標(biāo)上有 piece.y=k*piece.x+b
在目標(biāo)落點(diǎn)坐標(biāo)上有 des.y=kdes.x+b
代入得到 des.y=k*(des.x-piece.x)+piece.y
然而這種算法還是有偏差的。
image.gif可以看到,同樣的斜率,如果棋子的位置有偏差,計(jì)算出來(lái)最終落點(diǎn)還是會(huì)有偏差的。
代碼解析就先講這么多,希望有大神可以提出更好的解決方案。
玩游戲小竅門(mén)
連續(xù)的落到物體中心位置,是有分?jǐn)?shù)加成的,最多跳一次可以得幾十分
井蓋、商店、唱片、魔方,多停留一會(huì),有音樂(lè)響起后也是有分?jǐn)?shù)加成的
那么看一下程序員的朋友圈有多殘酷吧
原文作者:Samon
閱讀原文