[原創]用C#實現微信“跳一跳”小游戲的自動跳躍助手


一、前言:

前段時間微信更新了新版本后,帶來的一款H5小游戲“跳一跳”在各朋友圈里又火了起來,類似以前的“打飛機”游戲,這游戲玩法簡單,但加上了積分排名功能后,卻成了“裝逼”的地方,於是很多人花錢花時間的刷積分搶排名。后來越來越多的聰明的“程序哥們”弄出了不同方式不同花樣的跳一跳助手(外掛?),有用JS實現的、有JAVA實現的、有Python實現的,有直接物理模式的、有機械化的、有量尺子的等等,簡直是百花齊放啊……

趕一下潮流,剛好有點時間,於是花了一個下午時間,我也弄了一個C#版本的簡單實現。

 

二、實現:

簡單的實現流程: 連接手機 -> 獲取跳一跳游戲界面 -> 獲取位置(棋子位置和要跳躍的落腳點位置) -> 點擊棋子跳躍

1、連接手機

電腦要連接並操作安卓手機,一般是通過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

  

2、獲取游戲界面

獲取手機界面的截圖可通過以下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);

 

3、獲取棋子與跳躍落腳點位置

將獲取到的手機界面截圖顯示到軟件窗體上的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);
            }
        }

 

 

4、棋子跳躍

要能跳躍,首先需要知道一個蓄力時間,就是按住棋子多久的時間,此蓄力時間的計算公式如下:

蓄力時間 = 距離 * 力度系數

  

距離”就是棋子位置與跳躍落腳點位置的距離,根據上面的方法得出這兩個位置的坐標點后,根據直角三角形的勾股定理即可求出,代碼如下:

        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

 

聲明:本軟件、代碼和文章屬於本人原創,轉載請通知並注明原處!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM