Android 手機模擬游戲手柄(USB,C#,winio)
使用的知識點:Android服務器通過USB連接PC端,winio發送鍵盤消息,Socket編程,線程,Android多點觸控
先說下思路,首先在Android端開啟服務器程序,然后在PC端開啟一個服務器程序模擬發送鍵盤信息(C#編寫)。手機和PC用USB連接,Android和PC的通信通過Socket完成。
PC客戶端程序:
雖然有很多方法可以模擬發送鍵盤信息如:PostMessage,keybd_event等。這些都是將按鍵信息發送給系統的消息隊列,然后再響應。但是很多游戲使用了DirectX技術繞過了系統的消息隊列。
我用了一個開源的項目,winio。可以將鍵盤的信息直接發給主板,這樣一些游戲也可以接收了按鍵消息了。Winio的相關資料可以在網上搜到。由於我的系統是64位的,在使用過程中遇到了一些問題,主要是winio驅動簽名的問題。具體解決方法:http://www.cnblogs.com/wangqian0realmagic/archive/2012/03/26/2418671.html
我用VS2010進行客戶端的開發,這時動態載入winio64.dll時,會出現如下錯誤“System.DllNotFoundException……無法加載 DLL“WinIo64.dll”: 找不到指定的模塊。 (異常來自 HRESULT:0x8007007E)“。是因為VS2010內部平台默認是X86的,所以要改一下,生成->配置管理器->平台,設為X64即可。
PC端和Android端的USB通信要經過端口轉換,要在C#中動態使用adb.exe的forward命令。
代碼:
MsgTcpClient:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net.Sockets; using System.Net; namespace GameHandles { class MsgTcpClient { //數據定義 Socket msgClient; static int serverport = 60001; string ip; public MsgTcpClient() { msgClient = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp); } //嘗試連接如果成功返回true,失敗返回false public bool Connect(string ipstring) { ip = ipstring; IPEndPoint ipendpoint = new IPEndPoint(IPAddress.Parse(ip), serverport); try { msgClient.Connect(ipendpoint); return true; }catch { return false; } } //接收獲得的命令 public string getMsg() { string msgGot = ""; byte[] tmpmsg = new byte[8]; int length = 0; try { Console.WriteLine("start to recieve"); length = msgClient.Receive(tmpmsg, tmpmsg.Length, 0); msgGot = Encoding.ASCII.GetString(tmpmsg, 0, length); } catch { } return msgGot; } public void Close() { msgClient.Close(); } } }
主程序部分:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Net.Sockets; using System.Diagnostics; using System.Threading; namespace GameHandles { public partial class Form1 : Form { //數據定義 winioManpulate winioKey; MsgTcpClient msgClient;//接收Android服務器發來的信息 string serverip = "127.0.0.1"; Thread winioThread; Keys[] keycode = {Keys.A,Keys.W,Keys.D,Keys.S,Keys.U,Keys.J,Keys.K,Keys.I}; public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { winioKey = new winioManpulate(); msgClient = new MsgTcpClient(); } //控制winio private void changeKeys() { while(true) { string msgGot = ""; msgGot = msgClient.getMsg(); Console.WriteLine(msgGot); //Thread.Sleep(3000); if (msgGot.Equals("") == false) { for (int i = 0; i < msgGot.Length; ++i) { if (msgGot[i] == '1') { winioKey.KeyDown(keycode[i]); //label1.Text = "keydown"; } else { winioKey.KeyUp(keycode[i]); } } } } } //開始,設置adb,進行Tcp連接 private void btnConnect_Click(object sender, EventArgs e) { //設置adb Process adbprocess = new Process(); adbprocess.StartInfo.FileName = @"adb.exe"; adbprocess.StartInfo.Arguments = @"forward tcp:60001 tcp:60001"; adbprocess.Start(); Thread.Sleep(100); //連接server if (msgClient.Connect(serverip) == true)//如果連接成功 { winioKey.Initialize(); Thread.Sleep(100); btnStop.Enabled = true; btnConnect.Enabled = false; winioThread = new Thread(new ThreadStart(changeKeys)); winioThread.Start(); label1.Text = "begin"; } } private void btnStop_Click(object sender, EventArgs e) { winioThread.Abort(); msgClient.Close(); winioKey.Shutdown(); } } }
Android端:
Android作為socket服務器與普通的java程序差別不大,用ServerSocket類。只是要在AndroidManifest.xml中添加<uses-permission android:name="android.permission.INTERNET" />
還要將屏幕強制設為橫屏:setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
但是在實際編碼過程中遇到一些問題,我在onCreate中初始了ServerSocket對象,而setRequestedOrientation會重新執行onCreate(),這樣就重復初始化了ServerSocket對象,會提示地址已占用的錯誤。所以先將ServerSocket對象設為null,初始化之前先判斷是否為null,再初始化。
手柄的按鍵用多點觸控的技術實現。我寫了一個繼承View的類HandlePanel,在onDraw方法中繪制了8個按鍵。然后在Activity類中設置HandlePanel.setOnTouchListener,進行相關操作。這里又遇到一個問題,我第一次用的是Android2.1的系統,這版系統中不能精確的獲得是那個點出發了相應的事件,如:我在屏幕上按了兩只手指,抬起一只時,無法辨別是哪只手指抬起,只能同時獲得兩點的坐標。在網上也沒發現解決的辦法后來看Android的文檔,發現有個event.getActionIndex()的方法可以滿足需求,但是只在2.2以上的版本有,無奈啊。
與其他Socket編程一樣,ServerSocket對象要close掉,所以我重寫了Activity的onStop()方法。但是每次關閉時,都提示意外退出,所以要調用super.onStop();將原先的操作也執行一遍。小錯誤啊,,不過也要注意一下的。
代碼:
Socket服務器類:
package com.mhandle; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; public class MyServerSocket { ServerSocket MyServer; Socket mySocket; OutputStream os; Thread getMsg; public MyServerSocket() { // TODO Auto-generated constructor stub //MyServer = new ServerSocket(60001); mySocket = null; MyServer = null; } public void connect() throws Exception { if(MyServer == null) MyServer = new ServerSocket(60001); } public void sendMsg(String msg) throws IOException { if(MyServer != null) { if(mySocket == null) mySocket = MyServer.accept(); os = mySocket.getOutputStream(); PrintWriter pWriter = new PrintWriter(os); pWriter.write(msg); pWriter.flush(); } else { System.out.println("Out Error"); } } public void stop() throws IOException { if(mySocket != null) mySocket.close(); if(MyServer != null) MyServer.close(); } }
HandlePanel類:
package com.mhandle; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.os.IBinder; import android.util.AttributeSet; import android.view.Display; import android.view.MotionEvent; import android.view.SurfaceHolder; import android.view.View; import android.widget.RelativeLayout; public class HandlePanel extends View{ public HandlePanel(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // TODO Auto-generated constructor stub //init(); } public HandlePanel(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub //init(); } public HandlePanel(Context context) { super(context); // TODO Auto-generated constructor stub //init(); } Rect hRects[]; int rectwidth = 120; int VHeight; int VWidth; public void init(int h,int w) { VHeight = h; VWidth = w; System.out.println(VHeight+" "+VWidth); hRects = new Rect[8]; //方向左上右下,功能鍵的順序也是如此 int tops[] = {(VHeight-rectwidth)/2, (VHeight-rectwidth)/2-(rectwidth+10), (VHeight-rectwidth)/2, (VHeight-rectwidth)/2+(rectwidth+10), (VHeight-rectwidth)/2, (VHeight-rectwidth)/2-(rectwidth+10), (VHeight-rectwidth)/2, (VHeight-rectwidth)/2+(rectwidth+10) }; int lefts[] = { (VWidth/2-3*rectwidth-20)/2, (VWidth/2-3*rectwidth-20)/2+rectwidth+10, (VWidth/2-3*rectwidth-20)/2+2*rectwidth+20, (VWidth/2-3*rectwidth-20)/2+rectwidth+10, VWidth/2 + (VWidth/2-3*rectwidth-20)/2, VWidth/2 + (VWidth/2-3*rectwidth-20)/2 +rectwidth+10, VWidth/2 + (VWidth/2-3*rectwidth-20)/2 + 2*rectwidth+20, VWidth/2 + (VWidth/2-3*rectwidth-20)/2 + rectwidth+10, }; //System.out.println("left:" + (VWidth/2-3*rectwidth-20)/2); //System.out.println(h+" "+w); //for(int i=0;i<8;++i) // lefts[i] += 60; for(int i=0;i<8;++i) { hRects[i] = new Rect(); hRects[i].set(lefts[i],tops[i],lefts[i]+rectwidth, tops[i]+rectwidth); } } //判斷是否點擊到按鍵 public int inRect(int x, int y) { for(int i=0;i<8;++i) { if( x<hRects[i].right && x>hRects[i].left && y>hRects[i].top && y<hRects[i].bottom) return i; } return -1; } @Override public void onDraw(Canvas canvas) { //int width = 50; Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setColor(0xFFCBD2D8); //繪制按鍵 for(int i=0;i<8;++i) { canvas.drawRect(hRects[i], mPaint); } } }
教訓:要記錄錯誤的名稱(ClassNotFound之類),輸出catch中內容,讀技術文檔,將網上的demo或代碼測試一下,不能全信。