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或代码测试一下,不能全信。
<script type="text/javascript"> </script><script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>