最近这款“跳一跳”很火,在段子里面看到有人才放了张画着坐标的纸在手机上,说根据距离确定摁的“嘟”的次数,还有通过程序来实现自动计算的。看得心血来潮忍不住来试一试?话不多说,先上图。
因为比较急着做出成品,所以细节上没多细抠。感觉设置的跳跃速度稍快了一点,有兴趣的同学可以实测一下。也有一个因素是测试时后台程序比较多,影响了结果。
原理其实也是跟大家想的一样很简单,无非就是三个要素:距离、速度、时间。就是通过当前小蓝人脚底所在的像素坐标和目标平台中心像素的坐标计算距离,除以事先通过测试得出的速度,得出触摸屏幕时间,由程序发出“触摸”指令,实现定点跳跃。不过在做自动计算跳跃所需触摸时间之前还是要做一些准备功夫的。下面直接说一下详细的过程吧。
准备工作:
1、通过PS等工具获取①小蓝人最底下一行(作为当前位置Y坐标)和最左边一列(作为当前位置X坐标)的像素RGB,实测在本机基本都是一样的X(54,63, 102),Y(43, 43, 73)。图片左上角、右下角坐标分别为[0,0][Xmax,Ymax]。②获取小蓝人的头的宽度(所占像素点)。③获取左上角分数最底下一行的像素y坐标。
2、通过指令
adb shell input touchscreen swipe x y x y 延时(ms)
(x、y为触摸屏幕的坐标),结合photoshop测试出“跳一跳”每一条的速度。本例中测得结果约为17 / 24(pixel/ms),实际游戏中的速度略小于这个速度。大家用代码可以精确测一下,我已经没耐心了0.0。
3、电脑准备好调试环境(因为穷所以测试用的是自己的Android机,所以要准备好ADK(platform-tools/adb.exe);另外本次测试语言是C#)
4、手机开启调试模式,连接电脑,打开“跳一跳”
过程:
一、获取设备号(获取序列号,或者直接查看手机信息),指令:
adb devices
二、截取手机当前画面到sd卡(本机存储格式为png,实测手机按键截屏为jpg(失真)),指令:
adb -s 设备号 shell screencap -p /sdcard/temp.png
三、复制文件到电脑,指令:
adb -s 设备号 pull /sdcard/temp.png 保存路径
四、删除文件,指令:
adb -s 设备号 shell rm /sdcard/temp.png
五、获取小蓝人脚底像素坐标和目标平台中心像素坐标,下面详细说说里面的步骤
1、通过Bitmap类读取图片,再用unsafe代码利用指针把RGB数据直接从内存拷出来存放到byte数组中(这步其实不用也可以但不知道直接通过Bitmap获取像素效率会不会很低,大家可以测了分享一下结果)
2、用两层循环y从max->0,遍历x轴像素,通过对比找出小蓝人位置,本例通过两个rgb像素的标准差不超过3作为置信偏差判断两个像素是否为同一元素。再稍微处理一下就可得出当前坐标。
3、利用上面得到的坐标P以及一开始准备工作中提到的分数底行y坐标(取大于该y作为startY即可)再进行对目标坐标的搜索:用两层循环y从startY->Py,遍历x轴像素(利用P的x坐标缩小搜索的x坐标范围:若x位于左半屏则搜索Px+40->Xmax,反之搜索0->Px-40,注:不缩小范围会出错,原因大家想想)。(这个40可取大于小蓝人头宽度一半的值即可)
4、那就用我们的勾三股四弦五定理再开根求出距离。距离除以速度得出时间。
六、发送触摸指令实现定时跳跃,指令:
adb shell input touchscreen swipe x y x y延时(ms)
这里不得不说一下,当时找半天找不到定时触摸的指令,网上有个用6个指令组合实现定时触摸屏幕的方法,但实测无效,而且也怕指令这么多,延时还是分开控制,肯定会对跳跃结果有很大影响。后面看到一条利用swipe指令实现的评论,真是醒目。swipe虽然是滑动指令,但如果设置起止坐标都是同一个坐标不就相当于实现了定点定时触摸了吗。
七、七就是一直重复二~六的步骤就是了。
本次测试很东西都是急着做,没仔细研究,例如获取跳跃速度这个就是傻瓜式的通过手动发送跳跃指令、截图用ps手动计算出来的。大家可以用代码实现一下。希望大家指正可以改进的地方。
C#源码如下
Cmd类,实现cmd执行命令
class Cmd { private System.Diagnostics.Process process; private bool isExecuted; // 是否执行过命令 private string command; // 上次执行命令 private int result; // 上次执行命令结果 private string resultContent; // 上次执行命令返回结果 public Cmd() { process = new System.Diagnostics.Process(); process.StartInfo.FileName = "cmd.exe"; process.StartInfo.UseShellExecute = false; //是否使用操作系统shell启动 process.StartInfo.RedirectStandardInput = true;//接受来自调用程序的输入信息 process.StartInfo.RedirectStandardOutput = true;//由调用程序获取输出信息 process.StartInfo.RedirectStandardError = true;//重定向标准错误输出 process.StartInfo.CreateNoWindow = true;//不显示程序窗口 isExecuted = false; } public int ExecuteCmd(string cmd) { command = cmd; try { process.Start(); process.StandardInput.WriteLine(cmd + "&exit"); process.StandardInput.AutoFlush = true; string content = process.StandardOutput.ReadToEnd(); process.WaitForExit();//等待程序执行完退出进程 process.Close(); result = 0; resultContent = content.Split(new string[] { "&exit" }, StringSplitOptions.None)[1].Replace("\n", ""); } catch (Exception ex) { result = -1; resultContent = ex.Message; } if (!isExecuted) isExecuted = true; return result; } private int ExecuteCmd(string adbPath, string cmd) { command = $"\"{adbPath}\" {cmd}"; try { process.Start(); process.StandardInput.WriteLine(command + "&exit"); process.StandardInput.AutoFlush = true; string content = process.StandardOutput.ReadToEnd(); process.WaitForExit();//等待程序执行完退出进程 process.Close(); result = 0; resultContent = content.Split(new string[] { "&exit" }, StringSplitOptions.None)[1].Replace("\n", ""); } catch (Exception ex) { result = -1; resultContent = ex.Message; } if (!isExecuted) isExecuted = true; return result; } public string GetExcResult() { if (isExecuted) { if (result == 0) { return resultContent; } else { return $"Execute Failed! Command:{command}\n{resultContent}"; } } else { return "从未执行过命令"; } } public void DisposeProcess() { process.Dispose(); } } class Pixel { public byte[] pixel = new byte[3]; public Pixel() { } }
Pixel类,存放RGB字节
class Pixel { public byte[] pixel = new byte[3]; public Pixel() { } }
PlayJumpJump类,实现主要分析计算和跳跃操作
class PlayJumpJump { private static readonly int confidenceItv = 3; // 两个rgb标准差小于等于3认为是同一元素 private static readonly Pixel manXRgb = new Pixel { pixel = new byte[] { 54, 63, 102 } }; // 小人X坐标rgb private static readonly Pixel manYRgb = new Pixel { pixel = new byte[] { 43, 43, 73 } }; // 小人Y坐标rgb private static readonly double startYPer = 0.15625; // 分数下一行Y为第289,取 300 / 1920 = 0.15625, 从下一行开始搜索目标 private static readonly double Speed = 17.0 / 24; // 速度,最重要的因素,这也是约摸算出来的 private static readonly string[] TouchCoor = new string[] { "800", "1700" }; // 触屏位置 private static readonly string Format = "png"; // 本人用机子截取为png,也可不设格式(实测bitmap与ps cc打开同一jpg,同一像素点rgb值不一致,怀疑是bitmap打开jpg会有失真) private static readonly string TempDir = "/sdcard/"; private static readonly string SaveDir = "temp/"; private static readonly string CaptureScreen_Command = $"-s {{0}} shell screencap -p {TempDir}{{1}}"; private static readonly string CopyFile_Command = $"-s {{0}} pull {TempDir}{{1}} \"{SaveDir}{{1}}\""; private static readonly string RemoveFile_Command = $"-s {{0}} shell rm {TempDir}{{1}}"; private static readonly string LongPress_Command = "shell input touchscreen swipe {0} {1} {0} {1} {2}"; private Cmd myCmd; private string adbCmdPrefix; private string result; public List<string> devices; public PlayJumpJump(string adbPath) { myCmd = new Cmd(); adbCmdPrefix = $"\"{adbPath}\" "; if (!Directory.Exists(SaveDir)) { Directory.CreateDirectory(SaveDir); } } public void Init() { myCmd = new Cmd(); } public bool GetDevices() { devices = new List<string>(); myCmd.ExecuteCmd(ReturnCommand("devices")); result = myCmd.GetExcResult(); foreach (string line in result.Split(new char[] { '\n'})) { if (line.Contains("device")) { List<string> items = line.Split(new char[] { '\t', '\r' }, StringSplitOptions.None).ToList(); if (items.Count > 1) { devices.Add(items[items.IndexOf("device") - 1]); } } } return devices.Count > 0 ? true : false; } public string CaptureScreen() { string fileName = $"temp{DateTime.Now.ToString("HHmmssfff")}.{Format}"; myCmd.ExecuteCmd(ReturnCommand(CaptureScreen_Command, new string[] { devices[0], fileName })); myCmd.ExecuteCmd(ReturnCommand(CopyFile_Command, new string[] { devices[0], fileName })); myCmd.ExecuteCmd(ReturnCommand(RemoveFile_Command, new string[] { devices[0], fileName })); return AppDomain.CurrentDomain.BaseDirectory + SaveDir + fileName; } public static unsafe Pixel[][] GetPixelArray(string path) { Bitmap bitmap = new Bitmap(path); int depth = Image.GetPixelFormatSize(bitmap.PixelFormat); if (depth == 24) { int width = bitmap.Width; int height = bitmap.Height; Pixel[][] pixelArray = new Pixel[height][]; for (int i = 0; i < pixelArray.Length; i++) pixelArray[i] = new Pixel[width]; Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); BitmapData bmpData = bitmap.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); byte* ptr = (byte*)bmpData.Scan0; for (int i = 0; i < pixelArray.Length; i++) { for (int j = 0; j < pixelArray[i].Length; j++) { pixelArray[i][j] = new Pixel { pixel = new byte[] { *(ptr + 2), *(ptr + 1), *ptr } }; ptr += 3; } ptr += bmpData.Stride - 3 * bmpData.Width; // 减去占位字节(可能出于性能或兼容性考虑,Stride为4的倍数) } bitmap.UnlockBits(bmpData); return pixelArray; } else if (depth == 32) { int width = bitmap.Width; int height = bitmap.Height; Pixel[][] pixelArray = new Pixel[height][]; for (int i = 0; i < pixelArray.Length; i++) pixelArray[i] = new Pixel[width]; Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); BitmapData bmpData = bitmap.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppRgb); byte* ptr = (byte*)bmpData.Scan0; for (int i = 0; i < pixelArray.Length; i++) { for (int j = 0; j < pixelArray[i].Length; j++) { pixelArray[i][j] = new Pixel { pixel = new byte[] { *(ptr + 2), *(ptr + 1), *ptr } }; ptr += 4; // 每3个字节忽略1个透明度字节 } } bitmap.UnlockBits(bmpData); return pixelArray; } else { return null; } } public void Jump2Happy() { string picture = CaptureScreen(); Pixel[][] pixelArray = GetPixelArray(picture); int[] curCoor = GetCurCoordinates(pixelArray); int[] destCoor = GetDestCoordinates(pixelArray, curCoor); double distance = Math.Round(Math.Sqrt(Math.Pow(Math.Abs(destCoor[0] - curCoor[0]), 2) + Math.Pow(Math.Abs(destCoor[1] - curCoor[1]), 2)), 3); int time = (int)(distance / Speed); Console.WriteLine($"from [{curCoor[0]},{curCoor[1]}]\tto [{destCoor[0]},{destCoor[1]}] distance≈{distance} take≈{time}ms ==>> Jump "); myCmd.ExecuteCmd(ReturnCommand(LongPress_Command, new string[] { TouchCoor[0], TouchCoor[1], time.ToString() })); } public static int[] GetCurCoordinates(Pixel[][] pixelArray) { int[] coordinates = new int[2]; List<int[]> xList = new List<int[]>(); List<int[]> yList = new List<int[]>(); // y从max -> 0,遍历x轴像素 for (int i = pixelArray.Length - 1; i >= 0; i--) { for (int j = 0; j < pixelArray[i].Length; j++) { if (isSameElement(pixelArray[i][j], manXRgb, confidenceItv)) { xList.Add(new int[] { j, i }); } } if (xList.Count > 0) break; } coordinates[0] = xList.Count > 0 ? (xList[0][0] + xList[xList.Count - 1][0]) / 2 : 0; // x从0 -> max,遍历y轴像素 for (int i = 0; i < pixelArray[0].Length; i++) { for (int j = pixelArray.Length - 1; j >= 0; j--) { if (isSameElement(pixelArray[j][i], manYRgb, confidenceItv)) { yList.Add(new int[] { i, j }); } } if (yList.Count > 0) break; } coordinates[1] = yList.Count > 0 ? (yList[0][1] + yList[yList.Count - 1][1]) / 2 : 0; return coordinates; } public static int[] GetDestCoordinates(Pixel[][] pixelArray, int[] curCoor) { Pixel enviRgb; // 排除rgb采样 Pixel destRgb = null; // 采样 int[] coordinates = new int[2]; List<int[]> xList = new List<int[]>(); List<int[]> yList = new List<int[]>(); int startY = (int)(pixelArray.Length * startYPer); int start, end, inc; if (curCoor[0] < (pixelArray[0].Length / 2)) { start = curCoor[0] + 40; end = pixelArray[0].Length; } else { start = 0; end = curCoor[0] - 40; } // y从0 -> max,遍历x轴像素 for (int i = startY; i < pixelArray.Length; i++) { enviRgb = pixelArray[i][0]; for (int j = start; j < end; j++) { if (!isSameElement(pixelArray[i][j], enviRgb, confidenceItv)) { xList.Add(new int[] { j, i }); if (destRgb == null) destRgb = pixelArray[i][j]; } } if (xList.Count > 0) break; } coordinates[0] = xList.Count > 0 ? (xList[0][0] + xList[xList.Count - 1][0]) / 2 : 0; // x从0 -> max,遍历y轴像素 if (coordinates[0] < (pixelArray[0].Length / 2)) { start = 0; end = pixelArray[0].Length - 1; inc = 1; } else { start = pixelArray[0].Length - 1; end = 0; inc = -1; } bool isFond = false; for (int i = start; i != end; i+=inc) { for (int j = startY; j < curCoor[1]; j++) { if (isSameElement(pixelArray[j][i], destRgb, confidenceItv)) { coordinates[1] = j; isFond = true; break; } } if (isFond) break; } return coordinates; } public static bool isSameElement(Pixel pixel1, Pixel pixel2, int confidence) { return Math.Pow(pixel1.pixel[0] - pixel2.pixel[0], 2) + Math.Pow(pixel1.pixel[1] - pixel2.pixel[1], 2) + Math.Pow(pixel1.pixel[2] - pixel2.pixel[2], 2) <= 3 * Math.Pow(confidence, 2); } public string ReturnCommand(string command, string[] parameter) { return adbCmdPrefix + string.Format(command, parameter); } public string ReturnCommand(string command, string parameter) { return adbCmdPrefix + string.Format(command, parameter); } public string ReturnCommand(string command) { return adbCmdPrefix + command; } public void DisposeProcess() { myCmd.DisposeProcess(); myCmd = null; }
测试:
static void Main(string[] args) { string adbPath = ""; // adb.exe路径 PlayJumpJump testPlay = new PlayJumpJump(adbPath); if (testPlay.GetDevices()) { while (true) { testPlay.Jump2Happy(); Thread.Sleep(1200); } } testPlay.DisposeProcess(); Console.ReadKey(); } }
更多内容大家可以参考专题《微信跳一跳》进行学习。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。