一、開發環境
- 開發環境
使用語言:C/C++
IDE:VS2010+ - 其他三方庫
EasyX(http://www.easyx.cn/downloads/)
ADB(鏈接:https://pan.baidu.com/s/1ghjbm51 密碼:v68m) - ADB環境變量配置
打開adb文件夾,將此路徑添加到環境變量中;
手機使用數據線連接電腦,並打開USB調試;
打開cmd窗口,輸入adb devices
查看設備是否已連接,如圖:
adb devices -
EasyX安裝
雙擊打開安裝文件,選擇相應的VS版本即可,如圖:
easyx - 在VS中新建項目
vs2017 - 文件 - 新建 - 項目 - Win32控制台應用程序 - 空項目 - 完成
解決方案資源管理器 - 源文件 - 新建項 - 新建main.cpp
二、程序開發
2.1 使用adb操作手機
-
使用adb命令截屏
#include <stdio.h> #include <windows.h> #include <graphics.h> //EasyX庫 int main() { // 截屏並保存 system("adb shell screencap -p /sdcard/screen.png"); system("adb pull /sdcard/screen.png"); // ... return 0; }
運行后打開項目路徑檢查是否存在screen.png,如圖:
screen.png -
模擬長按屏幕
// 在程序中添加以下代碼: // 模擬手指按壓 // 按下坐標(200,300),離開坐標(205,305),按壓時間500ms system("adb shell input swipe 200 300 205 305 500");
打開微信跳一跳,運行程序,查看是否跳躍
2.2 處理截圖
-
目的
通過截圖獲取角色位置、待跳的方塊位置、以及它們之間的距離,進而計算屏幕按壓時間 -
為了在程序中處理截圖,需要將截圖格式轉化為jpg,添加部分代碼后程序如下:
#include <stdio.h> #include <windows.h> #include <graphics.h> #include "atlimage.h" // 處理圖像格式 IMAGE image; // 保存完整截圖 int main() { initgraph(1080,1920); // 初始化窗口,與截圖大小對應 system("adb shell screencap -p /sdcard/screen.png"); system("adb pull /sdcard/screen.png"); CImage cimage;// 圖像格式轉換 cimage.Load(L"screen.png"); cimage.Save(L"screen.jpg"); loadimage(&image, L"screen.jpg"); //將截圖保存到image變量中 putimage(0, 0, &image); //貼圖 // system("adb shell input swipe 200 300 205 305 500"); system("pause"); closegraph(); // 關閉圖形界面 return 0; }
運行程序,檢查窗體是否成功加載截圖,如圖:
運行窗體部分 -
由於截圖過大,且上下部分均不是游戲有效區域,故對圖像進行裁剪,取得必須部分,經計算取得800*700的大小即可:
#include <stdio.h> #include <windows.h> #include <graphics.h> #include "atlimage.h" // 處理圖像格式 IMAGE image,img; // 保存完整截圖與裁剪后截圖 int main() { initgraph(800,700); // 初始化窗口 system("adb shell screencap -p /sdcard/screen.png"); system("adb pull /sdcard/screen.png"); CImage cimage; // 圖像格式轉換 cimage.Load(L"screen.png"); cimage.Save(L"screen.jpg"); loadimage(&image, L"screen.jpg"); //將截圖保存到image變量中 SetWorkingImage(&image); // 修改工作區 getimage(&img, 100, 600, 800, 700); // 切割圖片 SetWorkingImage(NULL); // 恢復工作區 putimage(0, 0, &img); //貼圖 // system("adb shell input swipe 200 300 205 305 500"); system("pause"); closegraph(); // 關閉圖形界面 return 0; }
運行程序,如下:
運行窗體
2.3 角色、方塊定位
-
在獲取到游戲區域之后,便可以在此區域中通過顏色找到人物位置與方塊位置,而像素代表了顏色,故需要取到每一個像素點:
DWORD* pMem; //窗口顯存 int x,y; int xy[800][700]; // 保存img中所有的像素點 pMem = GetImageBuffer(NULL); //獲取窗口的顯存 SetWorkingImage(&img); for (y = 0; y < 700; y++) for (x = 0; x < 800; x++) xy[x][y] = getpixel(x, y); // 獲取像素點 // 貼圖... cleardevice(); // 清除屏幕 // 重新貼圖 for (x = 0; x < 800; x++) { for (y = 0; y < 700; y++) { int b = xy[x][y]&0xff; // 獲取低8位 int g = (xy[x][y] >> 8) & 0xff; // 低8位去掉,再取低8位 int r = xy[x][y] >> 16; // 取高16位 pMem[y * 800 + x] = BGR(xy[x][y]); } } FlushBatchDraw(); // 刷新緩存,顯示圖形
以上代碼可以獲取到img中的每一個像素點,並且以像素點重新進行貼圖
-
在獲取到所有點的顏色值之后,便可以開始根據顏色定位人物與跳躍點。從上到下開始逐像素掃描,則大部分情況下(極少數情況在后面處理)會先掃描到跳躍的方塊,故以此定位跳躍的方塊位置:
int xx1,yy1; // 方塊的頂部坐標 SetWorkingImage(&img); for (y = 0; y < 700; y++) { for (x = 0; x < 800; x++) { xy[x][y] = getpixel(x, y); // 獲取像素點 // 自上而下進行掃描,若掃描到了方塊的顏色,則跳出循環,不再獲取像素點 if (isNewBlock(xy[x][y])) // 判斷是否是新的盒子 { xx1 = x; yy1 = y; goto next; } } } next: // ... // 判斷是否是新的盒子 BOOL isNewBlock(int color){}
-
判斷人物的位置,人物的顏色是固定的,故較容易取到。人物分為頭和體,從上到下掃描則會掃描到頭,那么就從下到上開始掃描:
int peopleX,peopleY; // 人物坐標 next: for (y = 699; y >= 0; y--) // 自下而上開始掃描 { for (x = 0; x < 800; x++) { xy[x][y] = getpixel(x, y); // 獲取像素點 if (ColorFun(xy[x][y], RGB(55, 60, 100), 10)) //如果顏色是人物的顏色 { peopleX = x; // 獲得人物的坐標 peopleY = y; break; } } if (ColorFun(xy[x][y], RGB(55, 60, 100), 10)) break; } // 判斷顏色是否相似 BOOL ColorFun(COLORREF color1, COLORREF color2, int diff){}
-
在確定角色、方塊的位置后,即可以開始計算距離並跳躍。但剛才所用到的ColorFun()與isNewBlock()還仍未提供實現細節。
ColorFun()主要進行顏色相似的判斷:BOOL ColorFun(COLORREF color1, COLORREF color2, int diff) { // 取兩種顏色的R、G、B值 int r1 = GetRValue(color1); int g1 = GetGValue(color1); int b1 = GetBValue(color1); int r2 = GetRValue(color2); int g2 = GetGValue(color2); int b2 = GetBValue(color2); if (sqrt(double((r2-r1)*(r2-r1) + (g2-g1)*(g2-g1) + (b2-b1)*(b2-b1))) < diff) { return TRUE; } return FALSE; } /* sqrt(double((r2-r1)*(r2-r1) + (g2-g1)*(g2-g1) + (b2-b1)*(b2-b1)) 顏色由R、G、B三基色組成,兩個顏色值越接近,則其R、G、B值越接近 三種基色的顏色差的平方根的值則代表兩種顏色的相似度,值越小則越相似 該函數的diff參數即為相似度,可以手動指定來確定顏色相似的程度 */
isNewBlock()收集了跳一跳中絕大多數盒子模型的顏色:
BOOL isNewBlock(int color) { // color為在img中取到的像素 int r = GetRValue(color); int g = GetGValue(color); int b = GetBValue(color); if (colorFun(color, RGB(246, 246, 246), 10))//淺白色 return TRUE; else if (colorFun(color, RGB(250, 250, 250), 10)) return TRUE; else if (colorFun(color, RGB(255, 255, 255), 0))//純白色 return TRUE; else if (colorFun(color, RGB(100, 148, 106), 20))//墨綠色 return TRUE; else if (colorFun(color, RGB(113, 113, 113), 10))//深灰色 return TRUE; else if (colorFun(color, RGB(245, 128, 58), 10))//橙色 return TRUE; else if (colorFun(color, RGB(186, 239, 69), 10))//淺綠色 return TRUE; else if (colorFun(color, RGB(234, 203, 174), 10))//木質桌子 return TRUE; else if (colorFun(color, RGB(254, 240, 89), 10))//黃色 return TRUE; else if (r > 124 && r < 134 && g>111 && g < 121 && b > 219 && b < 229)//紫色相間 return TRUE; else if (r > 75 && r < 85 && g>158 && g < 165 && b > 85 && b < 95)//大圓綠柱子 return TRUE; else if (colorFun(color, RGB(254, 74, 83), 10))//紅色 return TRUE; else if (colorFun(color, RGB(152, 114, 111), 10))//華岩石 return TRUE; else if (colorFun(color, RGB(117, 117, 117), 10))//馬桶 return TRUE; else if (colorFun(color, RGB(225, 199, 142), 10)) return TRUE; else if (colorFun(color, RGB(241, 241, 241), 10))//書本 return TRUE; else if (colorFun(color, RGB(255, 172, 178), 10))//粉色盒子 return TRUE; else if (colorFun(color, RGB(73, 73, 73), 3))//奶茶杯子 return TRUE; else if (colorFun(color, RGB(147, 147, 147), 10))//類似唱片機 return TRUE; return FALSE; }
-
綜合以上內容,加入代碼:
IMAGE image; //保存圖片 IMAGE img; //保存需要的圖片,裁剪后 int xy[800][700]; //保存img中所有的像素點 DWORD* pMem; //窗口顯存 int x, y; int xx1, yy1; //第一個點的坐標,目標方塊頂的坐標 int peopleX, peopleY; //人的坐標 BOOL isNewBlock(int color); BOOL ColorFun(COLORREF color1, COLORREF color2, int diff); int main() { initgraph(800, 700); // 初始化窗口 pMem = GetImageBuffer(NULL); //獲取窗口的顯存 system("adb shell screencap -p /sdcard/screen.png"); system("adb pull /sdcard/screen.png"); CImage cimage;// 圖像格式轉換 cimage.Load(L"screen.png"); cimage.Save(L"screen.jpg"); loadimage(&image, L"screen.jpg"); //將截圖保存到image變量中 SetWorkingImage(&image);// 修改工作區 getimage(&img, 100, 600, 800, 700);// 切割圖片 SetWorkingImage(&img); for (y = 0; y < 700; y++) { for (x = 0; x < 800; x++) { xy[x][y] = getpixel(x, y); // 獲取像素點 if (isNewBlock(xy[x][y])) // 判斷是否是新的盒子 { xx1 = x; yy1 = y; goto next; } } } next: // 獲取角色位置 for (y = 699; y >= 0; y--) { for (x = 0; x < 800; x++) { xy[x][y] = getpixel(x, y); if (ColorFun(xy[x][y], RGB(55, 60, 100), 10)) { peopleX = x; peopleY = y; break; } } if (ColorFun(xy[x][y], RGB(55, 60, 100), 10)) break; } SetWorkingImage(NULL); // 恢復工作區 putimage(0, 0, &img); //貼圖 getchar(); cleardevice(); // 清除屏幕 //重新貼圖 for (x = 0; x < 800; x++) { for (y = 0; y < 700; y++) { int b = xy[x][y]&0xff; //獲取低8位 int g = (xy[x][y] >> 8) & 0xff; //低8位去掉,再取低8位 int r = xy[x][y] >> 16; //取高16位 pMem[y * 800 + x] = BGR(xy[x][y]); } } FlushBatchDraw(); //刷新緩存,顯示圖形 // system("adb shell input swipe 200 300 205 305 500"); system("pause"); closegraph(); // 關閉圖形界面 return 0; } BOOL isNewBlock(int color){} BOOL ColorFun(COLORREF color1, COLORREF color2, int diff){}
運行程序,如圖:
運行窗體
角色自下而上進行掃描,方塊自上而下進行掃描,在掃描到模型后停止掃描,故中間有一大塊是無顏色填充的,這樣就能更高效的定位人物和方塊。
2.4 計算和跳躍
-
在角色和方塊定位時都獲取到了坐標值,利用這個坐標值進行距離的計算,並根據距離與時間的關系計算長按屏幕的時間。
// 計算目標跳點 int xxx = xx1 + 10; int yyy = yy1 + 95; // 計算距離 int distance = sqrt(double((yyy - peopleY)*(yyy - peopleY) + (xxx - peopleX)*(xxx - peopleX))); // 計算時間 int time = 1.35 * distance; // 執行跳躍 sprintf(str, "adb shell input swipe 200 300 205 305 %d",time); system(str);
-
目標點的選定邏輯
在定位時獲取到了頂部坐標(xx1,yy1),那么是否需要底部坐標,進而求中心點呢?這么做更精確,但是每次跳躍都會跳到中心,顯然輔助被外掛檢測機制檢測到的概率更大,故選用下圖所示邏輯計算跳點:
頂點與方塊相切
如圖,以(x,y)作為方塊頂點,各種模型的切點與切線均如圖所示,以x和y增加部分偏移量而得出的目標點(綠線),對於小方塊來說更靠近中間,但是大方塊則基本不在中心,這樣既考慮了小方塊,又避免了外掛檢測。程序默認采用(x+10,y+95)進行跳點計算,可根據實際效果進行微調。 -
距離的計算
距離采用勾股定理進行計算 -
時間的計算
程序默認采用了1.35為系數進行時間計算,可根據實際效果進行微調。 -
此時程序如下:
IMAGE image; //保存圖片 IMAGE img; //保存需要的圖片,裁剪后 int xy[800][700]; //保存img中所有的像素點 DWORD* pMem; //窗口顯存 int x, y; int xx1, yy1; //第一個點的坐標,目標方塊頂的坐標 int peopleX, peopleY; //人的坐標 BOOL isNewBlock(int color); BOOL ColorFun(COLORREF color1, COLORREF color2, int diff); int main() { initgraph(800, 700); // 初始化窗口 pMem = GetImageBuffer(NULL); //獲取窗口的顯存 system("adb shell screencap -p /sdcard/screen.png"); system("adb pull /sdcard/screen.png"); CImage cimage;// 圖像格式轉換 cimage.Load(L"screen.png"); cimage.Save(L"screen.jpg"); loadimage(&image, L"screen.jpg"); //將截圖保存到image變量中 SetWorkingImage(&image);// 修改工作區 getimage(&img, 100, 600, 800, 700);// 切割圖片 SetWorkingImage(&img); for (y = 0; y < 700; y++) { for (x = 0; x < 800; x++) { xy[x][y] = getpixel(x, y); // 獲取像素點 if (isNewBlock(xy[x][y])) // 判斷是否是新的盒子 { xx1 = x; yy1 = y; goto next; } } } next: // 人物坐標 for (y = 699; y >= 0; y--) { for (x = 0; x < 800; x++) { xy[x][y] = getpixel(x, y); if (ColorFun(xy[x][y], RGB(55, 60, 100), 10)) { peopleX = x; peopleY = y; break; } } if (ColorFun(xy[x][y], RGB(55, 60, 100), 10)) break; } SetWorkingImage(NULL); // 恢復工作區 putimage(0, 0, &img); //貼圖 getchar(); cleardevice(); // 清除屏幕 //重新貼圖 for (x = 0; x < 800; x++) { for (y = 0; y < 700; y++) { int b = xy[x][y]&0xff; //獲取低8位 int g = (xy[x][y] >> 8) & 0xff; //低8位去掉,再取低8位 int r = xy[x][y] >> 16; //取高16位 pMem[y * 800 + x] = BGR(xy[x][y]); } } FlushBatchDraw(); //刷新緩存,顯示圖形 // 計算目標點 int xxx = xx1 + 10; int yyy = yy1 + 95; int distance = sqrt(double((yyy-peopleY)*(yyy-peopleY)+(xxx-peopleX)*(xxx-peopleX))); int time = 1.35 * distance; sprintf(str, "adb shell input swipe 200 300 205 305 %d", time); system(str); system("pause"); closegraph(); // 關閉圖形界面 return 0; } BOOL isNewBlock(int color){} BOOL ColorFun(COLORREF color1, COLORREF color2, int diff){}
2.5 優化改進
-
在2.4完成之后,運行程序可以實現單步計算和跳躍,若要自動跳躍,則添加循環即可。
-
關於圖形界面
在制作初期,圖形界面可以更直觀的顯示計算與定位過程,但在程序開發結束后,圖形顯示部分可注釋。 -
極少數情況無法識別跳躍點
在游戲中有一類極少遇到的情況,如下圖:
方塊無法被掃描
如圖,已跳過的方塊的縱坐標小於待跳方塊的縱坐標,所以會被先掃描到,對於這種情況,采用如下方案解決:先掃描角色,若角色x坐標小於400,則人物在左側,那么人物左側的像素就不必再被掃描,若人物在右側,則人物右側的內容也不必掃描。
-
反檢測
觸摸點:使用隨機數選擇觸摸點,使操作更接近人。程序默認方案為:srand((unsigned int)time(NULL)); // 使觸摸點在一個小范圍內隨機選擇 touchX = rand() % 80 + 200; // 200-279 touchY = rand() % 85 + 300; // 300-384
2.6 優化后代碼
#include <stdio.h> #include <windows.h> #include <graphics.h> #include "atlimage.h" #include <time.h> IMAGE image, img; // 截圖圖像 int coor[800][700]; // 保存截圖(處理后)中所有的像素點 int blockX, blockY; // 目標方塊頂部坐標 int peopleX, peopleY; // 角色的坐標 int touchX, touchY; // 模擬按下的坐標 int x, y; char str[100]; BOOL isNewBlock(int color); BOOL colorFun(COLORREF color1, COLORREF color2, int diff); int main() { srand((unsigned int)time(NULL)); while (1) { // 截圖並保存 printf("capturing data and screen:"); system("adb shell screencap -p /sdcard/screen.png"); system("adb pull /sdcard/screen.png"); // 截圖格式轉換 png -> jpg CImage cimage; cimage.Load(L"screen.png"); cimage.Save(L"screen.jpg"); loadimage(&image, L"screen.jpg"); // 把截圖保存到image // 切割圖片,取到游戲區,舍棄非游戲區域,加快判斷效率 SetWorkingImage(&image); getimage(&img, 100, 600, 800, 700); SetWorkingImage(&img); printf("getting the role and block location...\n"); // 掃描角色坐標,從下往上(從上往下掃描到頭部會停止) for (y = 699; y >= 0; y--) { for (x = 0; x < 800; x++) { coor[x][y] = getpixel(x, y);// 保存像素點 if (colorFun(coor[x][y], RGB(55, 60, 100), 10)) { peopleX = x; peopleY = y; goto getRole; } } } getRole: // 判斷角色在左邊還是右邊,加快圖形判斷效率,處理極小形方塊的bug for (y = 0; y < 700; y++) { for (x = ((peopleX < 400) ? peopleX + 60 : 0); x < ((peopleX < 400) ? 800 : peopleX - 60); x++) { coor[x][y] = getpixel(x, y); // 獲取像素點 if (isNewBlock(coor[x][y])) // 判斷是否是新的盒子 { blockX = x; blockY = y; goto getBlock; } } } getBlock: printf("calculate jump and touch coordinates...\n"); // 計算目標點 int targetX = blockX + 10; int targetY = blockY + 95; // 根據隨機數種子模擬手指按下坐標,防外掛檢測 touchX = rand() % 80 + 200; // 200-279 touchY = rand() % 85 + 300; // 300-384 int distance = sqrt(double((targetY - peopleY)*(targetY - peopleY) + (targetX - peopleX)*(targetX - peopleX))); int time = 1.35 * distance; sprintf(str, "adb shell input swipe %d %d %d %d %d", touchX, touchY, touchX + 1, touchY + 1, time); printf("executing:(%d,%d)->(%d,%d) touching (%d,%d) for %dms\n", peopleX, peopleY, targetX, targetY, touchX, touchY, time); system(str); Sleep(1500); } return 0; } // 判斷顏色是否相似,diff 越小越相似 BOOL colorFun(COLORREF color1, COLORREF color2, int diff) { return sqrt(double((GetRValue(color2) - GetRValue(color1))*(GetRValue(color2) - GetRValue(color1)) + (GetGValue(color2) - GetGValue(color1))*(GetGValue(color2) - GetGValue(color1)) + (GetBValue(color2) - GetBValue(color1))*(GetBValue(color2) - GetBValue(color1)))) < diff; } // 判斷是否是新的盒子 BOOL isNewBlock(int color) { int r = GetRValue(color); int g = GetGValue(color); int b = GetBValue(color); if (colorFun(color, RGB(246, 246, 246), 10))//淺白色 return TRUE; else if (colorFun(color, RGB(250, 250, 250), 10)) return TRUE; else if (colorFun(color, RGB(255, 255, 255), 0))//純白色 return TRUE; else if (colorFun(color, RGB(100, 148, 106), 20))//墨綠色 return TRUE; else if (colorFun(color, RGB(113, 113, 113), 10))//深灰色 return TRUE; else if (colorFun(color, RGB(245, 128, 58), 10))//橙色 return TRUE; else if (colorFun(color, RGB(186, 239, 69), 10))//淺綠色 return TRUE; else if (colorFun(color, RGB(234, 203, 174), 10))//木質桌子 return TRUE; else if (colorFun(color, RGB(254, 240, 89), 10))//黃色 return TRUE; else if (r > 124 && r < 134 && g>111 && g < 121 && b > 219 && b < 229)//紫色相間 return TRUE; else if (r > 75 && r < 85 && g>158 && g < 165 && b > 85 && b < 95)//大圓綠柱子 return TRUE; else if (colorFun(color, RGB(254, 74, 83), 10))//紅色 return TRUE; else if (colorFun(color, RGB(152, 114, 111), 10))//華岩石 return TRUE; else if (colorFun(color, RGB(117, 117, 117), 10))//馬桶 return TRUE; else if (colorFun(color, RGB(225, 199, 142), 10)) return TRUE; else if (colorFun(color, RGB(241, 241, 241), 10))//書本 return TRUE; else if (colorFun(color, RGB(255, 172, 178), 10))//粉色盒子 return TRUE; else if (colorFun(color, RGB(73, 73, 73), 3))//奶茶杯子 return TRUE; else if (colorFun(color, RGB(147, 147, 147), 10))//類似唱片機 return TRUE; return FALSE; }
三、程序運行
3.1 程序運行
- 安裝adb
- 配置adb環境變量
- 手機使用數據線連接電腦,並打開USB調試
- 打開cmd窗口,輸入
adb devices
檢測是否已連接 - 手機打開微信跳一跳
- 運行程序:如下圖
程序運行
3.2 程序修改
-
對於落地點的調整
// 計算目標點 int targetX = blockX + 10; // 修改 10 int targetY = blockY + 95; // 修改 95
-
對於距離與時間的關系微調
int time = 1.35 * distance; // 修改 1.35
-
對於新方塊模型的擴充
添加到isNewBlock()方法中即可