はじめに
みんな、演奏して、少なくとも一度は考えました:「私のために演奏するプログラムを書けたらいいのに!」 しかし、通常はこの考えなので、考えは残ります...何かが絶えず干渉します。どこから始めたらよいのか、タスクの不適切さを恐れ、左肩の後ろからささやきます。 プログラムで遊ぶためにこれが必要なのは誰ですか?」など。このシリーズの記事では、まず「悪魔は描かれているほどひどいものではない」ことを示し、次に、「なぜこれが必要なのか」という質問に答えます。
簡単なものから始めましょう。 ゲームとプレイヤープログラム(ボット)の関係の確立。 よく知られたズマのゲームは実験的なウサギと見なされます。
相互作用は2つのプロセスで構成されます。「それら」にデータを送信し、「それら」からデータを受信します。 Zumaでは、すべての制御はマウスで行われ、ゲームは画像を通じてフィードバックを提供します。 したがって、最初にマウスの動作をプログラムでエミュレートし、ゲームから画像を取得する方法を学ぶ必要があります。
この記事の主な目標は、独立して何度もゲームプレイに参加し、そこで何かを行い、ゲームオーバーを行うと、プログラムが最初からやり直されるプログラムを取得することです。 さらに、このフレームワークは、ゲームが終わるまでボットがゲーム内でさらに長くなるように発展します。
解決された補助サブタスク: マウスエミュレーション、仮想マシンへのマウスリダイレクト、画像キャプチャ。
リトリート
この一連の記事のコードを開発するとき、アプローチが使用されます:最小限の労力でできるだけ早く結果を得るために。 このアプローチにより、モチベーションを高いレベルで維持でき、タスクが重すぎると判断してもgiveめません。 このため:-最初に、多くの重要でない(現在の結果の観点から)瞬間がすぐに実行され、コードに「サポートとサポート」が残ります。 そして、これらの瞬間が個別に理解されるのは次の反復でのみであり、「クランチ」は本格的なコードに置き換えられます。
-次に、コードスタイルは従来のC#よりも「ハッカー」です。 コードには、多くのラムダ、匿名データ、トリック、著作権のbit意性、およびコメントの完全な欠如が含まれます。
マウスエミュレーション
Windowsは、4つの異なるWinApi関数を使用してマウスをエミュレートする2つの標準的な方法をサポートしています。最初の方法 : SendMessageまたはPostMessage関数を使用して、プログラムにウィンドウメッセージ( WM_MOUSEMOVE 、 WM_LBUTTONDOWNなど)を送信します。
DirectXゲームの場合(この場合のように)、この方法は適切ではありません。マウスをポーリングするプログラムではDirectInputを使用するため、Windowsメッセージを無視してマウスを直接ポーリングします。
2番目の方法 : mouse_eventまたはSendInput関数を使用して、マウスの動作を直接エミュレーションします 。 この方法は、フルスクリーンのDirectXゲームを含むあらゆるプログラムに適しています。 mouse_event関数は単純ですが、廃止されたと見なされており、SendInputはより現代的ですが、より面倒です。 mouse_eventを見てみましょう。
C#のWinApi関数は、 PInvokeテクノロジーを使用して呼び出されます。 最も一般的なWinApi関数のPInvokeの説明は、 PInvoke.net Webサイトにあります。 mouse_event関数も例外ではありません。
[DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, UIntPtr dwExtraInfo);
マウス座標
mouse_event関数には特定の機能があります。マウス座標はピクセルではなくミッキーで指定されます。 ミッキーのピクセルへの再計算(およびその逆)は、使用するメインモニターの解像度に依存します。 (0,0)はモニターの左上隅に対応し、(65535、65535)は右下に対応します。これは、mickey_point = pixel_point * (65536, 65536) / screen_size
および
pixel_point = mickey_point * screen_size / (65536, 65536)
。
基本操作
上記のすべてをまとめると、マウスを制御するための次の操作が得られます。マウスカーソルをポイント(x、y)に移動します。
mouse_event(MouseEventFlags.MOVE | MouseEventFlags.ABSOLUTE, x * 65536 / screen_width, y * 65536 / screen_height);
左クリック:
mouse_event((MouseEventFlags.LEFTDOWN), 0, 0); System.Threading.Thread.Sleep(100); mouse_event((MouseEventFlags.LEFTUP), 0, 0);
右クリック:
mouse_event((MouseEventFlags.RIGHTDOWN), 0, 0); System.Threading.Thread.Sleep(100); mouse_event((MouseEventFlags.RIGHTUP), 0, 0);
問題:入力の排他性
mouse_event関数を介してマウスをエミュレートする場合、深刻な不便があります。mouse_eventは、個別のアプリケーションではなく、OS全体のマウスを一度にシミュレートします。 その結果、ボットの実行中および再生中は、コンピューターでのその他の作業(ボットのデバッグ、ボットの状態の積極的な表示、インターネットの読み取りなど)は不可能になります。 しかし、方法があります:仮想マシンです!ゲームを仮想マシンに転送する
ゲームを仮想マシンに転送すると、次の問題が解決します。-まず、ウィンドウモードをサポートせず、フルスクリーンモードでのみ動作するゲームとのやり取りを簡素化します。
-次に、マウス入力は仮想マシン上でのみ置換され、メインマシン上では通常モードで動作し続けるため、コンピューターユーザーは自分の仕事に取り掛かることができます。
ボットは、ゲーム自体とは異なり、メインマシンで実行する方が便利です。 これにより、Visual Studioから直接ボットを再起動し、そこでデバッグしたり、ボットの内部状態を表示したりすることができます。
仮想マシン(この場合、 Oracle VirtualBoxを使用 )のデプロイ、ゲストOSのインストール、およびゲームの転送は、1つの点を除いて通常行われます。ボットには、ホストOSとゲストOS間のネットワーク通信を確立する機能が必要です。 これはさまざまな方法で行われます。 1つの方法は、VirtualBoxを使用して特定のポートをゲストOSからホストに転送することです。 別の方法は、ブリッジアダプターモードを構成することです。ネットワーク全体の仮想マシンは通常のコンピューターのように見え、ゲストOSはルーターからdhcpを介してIPアドレスを受け取ります。 このアドレスは、ホストOSからゲストにアクセスされます。 (この場合、著者はブリッジアダプターでオプションを使用しました)
プロキシ
ゲストOSのマウスを制御するために、単純なtcpコンソールサーバーであるプロキシを作成します。 その完全なコードは小さく、カットの下に表示されます。 コードを簡素化し、依存関係を減らすために、プロキシは、リモート処理、wcfなどを使用せずに、ベアソケットに書き込まれます。 プロキシコード
using System; using System.Collections.Generic; using System.Linq; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text; namespace InputProxy { class Program { static void Main(string[] args) { var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Bind(new System.Net.IPEndPoint(System.Net.IPAddress.Any, 7001)); socket.Listen(10); for (; ; ) { var client = socket.Accept(); Console.WriteLine("connected.."); var thread = new System.Threading.Thread(() => { try { var clientReader = new System.IO.BinaryReader(new NetworkStream(client)); for (; ; ) { if (client.Poll(1, SelectMode.SelectRead) && client.Available == 0) { Console.WriteLine("disconnected.."); break; } if (client.Available > 0) { var msgSize = clientReader.ReadInt32(); var message = clientReader.ReadBytes(msgSize); var messageReader = new System.IO.BinaryReader(new System.IO.MemoryStream(message)); var msgKind = messageReader.ReadInt32(); Console.WriteLine("message: kind:{0}, len:{1}", msgKind, message.Length); switch (msgKind) { case 0: { var flags = messageReader.ReadUInt32(); var x = messageReader.ReadInt32(); var y = messageReader.ReadInt32(); var data = messageReader.ReadUInt32(); mouse_event(flags, x, y, data, UIntPtr.Zero); } break; } } else System.Threading.Thread.Sleep(10); } } catch (Exception exc) { Console.WriteLine(exc); } }) { IsBackground = true }; thread.Start(); } } [DllImport("user32.dll")] public static extern void mouse_event(uint dwFlags, int dx, int dy, uint dwData, UIntPtr dwExtraInfo); } }
プロキシが機能するには、仮想マシンにコピーして実行するだけです。 プロキシは、ポート7001でメッセージを待機し、その作業のログをコンソールに表示します。 プロキシを完了するには、コンソールウィンドウを閉じます。
お客様
プロキシへの接続は、プロキシコード自体よりも簡単です。 var client = new System.Net.Sockets.TcpClient(vm_host, 7001); var clientStream = client.GetStream(); var clientWriter = new System.IO.BinaryWriter(clientStream); Action<MouseEventFlags, int, int> mouse_event = (flags, x, y) => { var messageStream = new System.IO.MemoryStream(); var messageWriter = new System.IO.BinaryWriter(messageStream); messageWriter.Write(0); messageWriter.Write((uint)flags); messageWriter.Write(x); messageWriter.Write(y); messageWriter.Write(0); var message = messageStream.ToArray(); clientWriter.Write(message.Length); clientWriter.Write(message); clientStream.Flush(); };
画像キャプチャ
画像は、画面から直接キャプチャするのが最も簡単です。 .netには、このための既製の関数Graphics.CopyFromScreenがあります。 この方法について詳しく説明します。まず、出力ではグラフィックではなくビットマップを取得します-これは補助関数を使用して解決されます:
public static Bitmap GetScreenImage(Rectangle rect) { var bmp = new Bitmap(rect.Width, rect.Height, PixelFormat.Format32bppArgb); using (Graphics graphics = Graphics.FromImage(bmp)) { graphics.CopyFromScreen(rect.Left, rect.Top, 0, 0, rect.Size, CopyPixelOperation.SourceCopy); } return bmp; }
次に、画面のどの部分をキャプチャする必要があるかを知る必要があります。 もちろん、常に画面の同じ部分をキャプチャし、画面のこの部分に手でゲームを置くことができますが、これは
両方の関数のpinvokeの説明は、pinvoke.netで入手できます: FindWindowおよびGetWindowRect 。
[DllImport("user32.dll", SetLastError = true)] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; }
また、仮想マシンウィンドウのイメージキャプチャコードは次のとおりです。
var vm_left = 8; var vm_right = 8; var vm_top = 50; var vm_bottom = 30; var vm_title = "Windows81 [Running] - Oracle VM VirtualBox"; var handle = FindWindow(null, vm_title); if (handle == IntPtr.Zero) throw new Exception(" "); RECT rect; GetWindowRect(handle, out rect); var gameScreenRect = new System.Drawing.Rectangle(rect.Left + vm_left, rect.Top + vm_top, rect.Right - rect.Left - vm_right - vm_left, rect.Bottom - rect.Top - vm_bottom - vm_top); var gameBmp = GetScreenImage(gameScreenRect);
弱点
このアプローチの重大な欠点は、キャプチャされたウィンドウが、まず:画面全体に配置され、次に:他のすべてのウィンドウの上に配置される必要があることです。 この不便さは、2つ(またはそれ以上)のモニターの助けを借りて平準化されます。 また、この問題は、前述の方法を使用して完全に解決されます。つまり、仮想マシン内で関数を転送(画面キャプチャ)します。 これを行うには、適切な関数をInputProxyに追加するだけです。ループゲームプレイ
最後に、今日の課題の解決策に直接進みます。ゲームプロセスをループさせる-必要なすべてのサブタスクが解決されます。 Zumaのゲームプレイは、メイン、ミッション、アクションの3つのウィンドウを中心に展開します。 メインウィンドウにはメインメニューがあり、ゲームの種類を選択できます。ミッションウィンドウではミッションを選択でき、ゲームプロセスはアクションウィンドウで実行されます。ボットは、最も単純な方法で現在のウィンドウを決定します:複数のキーポイントの色の値によって。 ポイントは手動で選択されます:「近いピアリング」の方法によって。
var screenChecks = new[] { new { Name = "main", Points = new[] { new CheckPoint(200, 190, 0xff554a22), new CheckPoint(65, 400, 0xfff44c41) } }, new { Name = "mission", Points = new[] { new CheckPoint(200, 190, 0xffb5d0c7), new CheckPoint(65, 400, 0xffad7630) } }, new { Name = "action", Points = new[] { new CheckPoint(950, 10, 0xff72554b), new CheckPoint(10, 10, 0xff462b1d), } }, }; Func<Bitmap, string> check = image => screenChecks.Where(_check => image.Check(_check.Points)).Select(_check => _check.Name).FirstOrDefault();
ボットのメインサイクル:
var startButtonPoint = new Point(950, 430); var startMissionPoint = new Point(600, 750); for (; ; ) { try { var bmp = GetScreenImage(gameScreenRect); var screenName = check(bmp); Console.Write(screenName + new string(' ', 20) + new string('\x8', 40)); switch (screenName) { case "main": mouse_event(MouseEventFlags.MOVE | MouseEventFlags.ABSOLUTE, startButtonPoint.X * 65536 / game_width, startButtonPoint.Y * 65536 / game_height); System.Threading.Thread.Sleep(400); mouse_event(MouseEventFlags.LEFTDOWN, 0, 0); System.Threading.Thread.Sleep(150); mouse_event(MouseEventFlags.LEFTUP, 0, 0); System.Threading.Thread.Sleep(50); System.Threading.Thread.Sleep(400); break; case "mission": mouse_event(MouseEventFlags.MOVE | MouseEventFlags.ABSOLUTE, startMissionPoint.X * 65536 / game_width, startMissionPoint.Y * 65536 / game_height); System.Threading.Thread.Sleep(10); mouse_event(MouseEventFlags.LEFTDOWN, 0, 0); System.Threading.Thread.Sleep(150); mouse_event(MouseEventFlags.LEFTUP, 0, 0); System.Threading.Thread.Sleep(50); break; case "action": mouse_event(MouseEventFlags.LEFTDOWN, 0, 0); System.Threading.Thread.Sleep(150); mouse_event(MouseEventFlags.LEFTUP, 0, 0); System.Threading.Thread.Sleep(50); break; case null: bmp.Save("unknown.bmp"); break; } } catch (Exception exc) { Console.WriteLine(exc); } }
ゲームフェーズでは、ボットは常にクリックし、ある時点でボールをリリースします。 このような単純な(むしろ愚かな)戦術では、最初のミッションのボットは1000-2000ポイントを獲得し、時には完全にズマストリップを獲得することさえあります。
まとめ
目標が達成されます。ボットのフレームが作成され、ゲームプレイがループします。次の目標:OpenCVを接続し、ボールの位置と色を認識します。
Ps
注目を集めるイメージ。 (オレンジは、ボットの次のバージョンがボールとして認識した領域を示します)
DirectXアーケード用のボット。 部品番号1:連絡する
アーケード用のボット。 部品番号2:OpenCvを接続する