前段时间微信更新了新版本后,带来的一款 H5 小游戏 "跳一跳" 在各朋友圈里又火了起来,类似以前的 "打飞机" 游戏,这游戏玩法简单,但加上了积分排名功能后,却成了 "装逼" 的地方,于是很多人花钱花时间的刷积分抢排名。后来越来越多的聪明的 "程序哥们" 弄出了不同方式不同花样的跳一跳助手(外挂?),有用 JS 实现的、有 JAVA 实现的、有 Python 实现的,有直接物理模式的、有机械化的、有量尺子的等等,简直是百花齐放啊……
赶一下潮流,刚好有点时间,于是花了一个下午时间,我也弄了一个 C# 版本的简单实现。
简单的实现流程: 连接手机 -> 获取跳一跳游戏界面 -> 获取位置(棋子位置和要跳跃的落脚点位置) -> 点击棋子跳跃
电脑要连接并操作安卓手机,一般是通过 ADB 协议连接手机并进行操作。连接手机前要求手机已开启 USB 调试模式,可通过 USB 线或者 TCP 方式连接手机。正常只要电脑安装了 adb sdk tools 之类的工具包,就会自带有 adb 命令,所以 C# 要能操作手机,简单实现就是直接利用现成的 adb 命令。
手机通过 USB 线接入电脑后,在 CMD 窗口输入以下 adb devices 命令,如果显示有 device 列表则表示手机已连接成功可以对手机进行操作了。
- C:\Users\k>adb devices
- List of devices attached
- e832acb device
获取手机界面的截图可通过以下 adb 命令获取:
- adb shell screencap - p[filename]
参数 :
- p 表示截图保存格式为 PNG 图像格式。
filename: 截图保存的路径地址(手机路径),如果不输入则将截图数据直接输出到当前控制台会话,否则会将截图保存到相关路径地址(必须有写权限)
为避免文件保存到手机后还要再执行 adb pull(拉文件到本地电脑)的操作,所以选择不带 filename 参数的命令。在 C# 代码里通过 Process 这个类进行 adb 命令的调用执行,实现代码如下:
- var startInfo = new ProcessStartInfo("adb", "shell screencap -p");
- startInfo.CreateNoWindow = true;
- startInfo.ErrorDialog = true;
- startInfo.RedirectStandardOutput = true;
- startInfo.UseShellExecute = false;
- var process = Process.Start(startInfo);
- process.Start();
- var memoStream = new MemoryStream();
- process.StandardOutput.BaseStream.CopyTo(memoStream);
但由于 adb client 的原因,在它输出的截图数据流中会对'\n'(0A) 这个字符替换为''\r\n'(0D0A) 这两个字符,并且在测试中还发现不同的手机替换次数还不相同的,有可能替换一次,也有可能替换二次!所以为解决这个问题,先计算在最开始的 10 字节里的数据出现了多少次'\r'(0D) 字符后再出现'\n'(0A) 字符,因为正常的 PNG 文件,在文件头的第 4,第 5 个字节位置里会有'\r\n'(0D0A) 标志,所以检查出来的出现次数就表示'\n'(0A) 被 adb client 替换了多少次,之后再对整个接收到的数据流进行'\n'(0A) 还原(删除无用的'\r'(0D) 字符)。
>> 统计'\n'被替换了次
- private static int Find0DCount(MemoryStream stream)
- {
- int count = 0;
- stream.Position = 0;
- while(stream.Position < 10 && stream.Position < stream.Length)
- {
- int b = stream.ReadByte();
- if(b == '\r')
- {
- count++;
- }
- else if(b == '\n')
- {
- return count;
- }else if(count > 0)
- {
- count = 0;
- }
- }
- return 0;
- }
>> 对接受到的截图数据流进行'\n'字符还原
- var count = Find0DCount(memoStream);
- var newStream = new MemoryStream();
- memoStream.Position = 0;
- while (memoStream.Position != memoStream.Length)
- {
- var b = memoStream.ReadByte();
- if (b == '\r')
- {
- int c = 1;
- var b1 = memoStream.ReadByte();
- while(b1 == '\r' && memoStream.Position != memoStream.Length)
- {
- c++;
- b1 = memoStream.ReadByte();
- }
- if(b1 == '\n')
- {
- if(c == count)
- {
- newStream.WriteByte((byte)'\r');
- }
- newStream.WriteByte((byte)b1);
- }
- else
- {
- for(int i=0; i<c; i++) newStream.WriteByte((byte)'\r');
- newStream.WriteByte((byte)b1);
- }
- }
- else {
- newStream.WriteByte((byte)b);
- }
- }
- return new Bitmap(newStream);
将获取到的手机界面截图显示到软件窗体上的 PictureBox 控件上,可用鼠标的左右键分别点击图片位置标示棋子位置和需要跳的落脚点位置,鼠标点击的坐标位置即表示手机界面的坐标位置。由于手机界面截图在 PictureBox 控件显示时为了能一屏全图显示,对图片做了缩放处理,且图片缩放后如果图片的宽度小于 PictureBox 控件的宽度,PictureBox 会将图片居中后显示。所以鼠标点击的坐标位置还需要进行坐标转换才可以映射为手机界面里的绝对坐标位置。
转换计算方法:先计算 PictureBox 控件的图片缩放值和图片显示的左边距,然后再对鼠标点击坐标进行缩放计算。代码如下:
- private Point CalPoint(Point p)
- {
- if (this.cbZoom.Checked && this.pictureBox1.Image != null)
- {
- var zoom = (double)this.pictureBox1.Height / this.pictureBox1.Image.Height;
- var width = (int)(this.pictureBox1.Image.Width * zoom);
- var left = this.pictureBox1.Width / 2 - width / 2;
- return new Point((int)((p.X - left) / zoom), (int)(p.Y / zoom));
- }
- else
- {
- return p;
- }
- }
如全靠手动鼠标点击坐标位置来玩游戏,这和直接在手机里手动玩游戏是没有什么区别的,区别只在于能够跳跃精准些(跳跃力度能自动计算出,下面会讲),所以程序还要能够实现自动化,就是要能够自动找出棋子与跳跃落脚点的位置。
A、找棋子的坐标位置棋子的位置非常的好找,对游戏界面里的棋子(图 2 黄色块)进行放大可以发现棋子底部有一块区域(图 3 白色块)的颜色值是固定的 R(54)G(60)B(102) 颜色,如下两图:
(图 2)
(图 3)
根据棋子的这一颜色特点在获取到手机界面截图时,对图片象素进行扫描,查找 R(54)G(60)B(102) 这一颜色,找到的坐标位置就是棋子的位置。为了能快速扫描图片,不采用效率较低下的 GetPixel 方法获取颜色值,而采用 LockBits 方法锁定图片数据到内存,再采用指针移动获取象素颜色,由于采用了指针,代码需要开启 unsafe 定义。且棋子正常情况下不会在最顶部和最底部出现,所以不需要对整张界面图片扫描,只扫描 20%-63% 区域的数据,并且从底部开始找起。
B、找跳跃的落脚点位置写此助手只是无聊时的产出物,所以我只是简单实现。游戏中如果连续跳到了目标物的中间位置时,新目标物的中间部分会出现一个白色圈(如上图 2 的红色块),如果再跳中此位置,会进行加分。根据这一特点,程序找出那一白色圈圈的位置即可做为落脚点位置,白色圈的颜色值为 R(254)G(254)B(254),如果没有此白色圈位置,则手动鼠标选择落脚点位置。实现此功能后,程序基本上也能实现 90% 左右的自动化跳跃了。
查找代码实现如下:
- private static Point FindPointImpl(Bitmap bitmap, out Point comboPoint)
- {
- var standPColor = Color.FromArgb(54, 60, 102);
- var comboPColor = Color.FromArgb(245, 245, 245);
- Point standPoint = Point.Empty;
- comboPoint = Point.Empty;
- int y1 = (int)(bitmap.Height * 0.2);
- int y2 = (int)(bitmap.Height * 0.63);
- PixelFormat pf = PixelFormat.Format24bppRgb;
- BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, y1, bitmap.Width, y2), ImageLockMode.ReadOnly, pf);
- try
- {
- unsafe
- {
- int w = 0;
- while (y2 > y1)
- {
- byte* p = (byte*)bitmapData.Scan0 + (y2 - y1 - 1) * bitmapData.Stride;
- w = bitmap.Width;
- int endColorCount = 0;
- while (w > 40)
- {
- ICColor* pc = (ICColor*)(p + w * 3);
- if (standPoint == Point.Empty &&
- pc->R == standPColor.R && pc->G == standPColor.G && pc->B == standPColor.B)
- {
- standPoint = new Point(w - 3, y2);
- if (comboPoint != Point.Empty) break;
- }
- else if (comboPoint == Point.Empty)
- {
- if (pc->R == comboPColor.R && pc->G == comboPColor.G && pc->B == comboPColor.B)
- {
- endColorCount++;
- }
- else
- {
- if (endColorCount > 0)
- {
- comboPoint = new Point(w + 5, y2 - 1);
- if (standPoint != Point.Empty) break;
- }
- endColorCount = 0;
- }
- }
- w--;
- }
- if (comboPoint == Point.Empty)
- {
- if (endColorCount > 10)
- {
- comboPoint = new Point(w + 5, y2 - 1);
- }
- }
- if (standPoint != Point.Empty && comboPoint != Point.Empty) break;
- y2--;
- }
- }
- return standPoint;
- }
- finally
- {
- bitmap.UnlockBits(bitmapData);
- }
- }
要能跳跃,首先需要知道一个蓄力时间,就是按住棋子多久的时间,此蓄力时间的计算公式如下:
- 蓄力时间 = 距离 * 力度系数
"距离" 就是棋子位置与跳跃落脚点位置的距离,根据上面的方法得出这两个位置的坐标点后,根据直角三角形的勾股定理即可求出,代码如下:
- public double Distance
- {
- get
- {
- if (!this.CanDo) return -1;
- int w = Math.Abs(this.P2.X - this.P1.X);
- int h = Math.Abs(this.P2.Y - this.P1.Y);
- return Math.Sqrt((double)(w * w) + (h * h));
- }
- }
"力度系数" 是一个常量值,具体怎么定义没去细查,我采用的计算公式是: "力度系数 = 1495 / 手机分辨率的宽度值", 如我的手机分辨率是 1080*1920,则力度系数就是 1495 / 1080 = 1.3842....
算出了蓄力时间后通过以下 adb 命令发送到手机即可模拟点击操作。
- adb shell input swipe < x1 > <y1 > <x2 > <y2 > [duration(ms)]
x1, y1 就是棋子的坐标位置
x2, y2 还是棋子的坐标位置
duration 蓄力时间值,由距离 * 力度系数得出。
代码如下:
- public bool Do()
- {
- if (!this.CanDo) return false;
- var startInfo = new ProcessStartInfo("adb", string.Format("shell input swipe {0} {1} {0} {1} {2}", this.P1.X, this.P1.Y, this.Time));
- startInfo.CreateNoWindow = true;
- startInfo.ErrorDialog = true;
- startInfo.UseShellExecute = false;
- var process = Process.Start(startInfo);
- return process.Start();
- }
程序实现很简单,都是通过 adb 命令与手机进行交互操作。如果你认为对你有帮助麻烦赞下即可:)积分别玩太过哦。
可执行文件下载地址: JumperHelper.rar
代码仓库: https://github.com/kingthy/JumperHelper
声明:本软件、代码和文章属于本人原创,转载请通知并注明原处!来源: https://www.cnblogs.com/kingthy/p/jumperhelper.html