跳一跳自动跳跃C#代码实现

 更新时间:2020年6月25日 11:17  点击:1737

      最近这款“跳一跳”很火,在段子里面看到有人才放了张画着坐标的纸在手机上,说根据距离确定摁的“嘟”的次数,还有通过程序来实现自动计算的。看得心血来潮忍不住来试一试?话不多说,先上图。

       因为比较急着做出成品,所以细节上没多细抠。感觉设置的跳跃速度稍快了一点,有兴趣的同学可以实测一下。也有一个因素是测试时后台程序比较多,影响了结果。
       原理其实也是跟大家想的一样很简单,无非就是三个要素:距离、速度、时间。就是通过当前小蓝人脚底所在的像素坐标和目标平台中心像素的坐标计算距离,除以事先通过测试得出的速度,得出触摸屏幕时间,由程序发出“触摸”指令,实现定点跳跃。不过在做自动计算跳跃所需触摸时间之前还是要做一些准备功夫的。下面直接说一下详细的过程吧。

准备工作:

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(); 
 } 
 } 

更多内容大家可以参考专题《微信跳一跳》进行学习。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持猪先飞。

[!--infotagslink--]

相关文章

  • C#实现简单的登录界面

    我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • C#中截取字符串的的基本方法详解

    这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
  • C#实现简单的Http请求实例

    这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#中new的几种用法详解

    本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
  • 使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)

    这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#开发Windows窗体应用程序的简单操作步骤

    这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • 轻松学习C#的基础入门

    轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
  • C#变量命名规则小结

    本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
  • C#绘制曲线图的方法

    这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • C# 中如何取绝对值函数

    本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
  • c#自带缓存使用方法 c#移除清理缓存

    这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
  • c#中(&&,||)与(&,|)的区别详解

    这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • 经典实例讲解C#递归算法

    这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
  • C#学习笔记- 随机函数Random()的用法详解

    下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#中list用法实例

    这篇文章主要介绍了C#中list用法,结合实例形式分析了C#中list排序、运算、转换等常见操作技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25