哈工大计算机科学与技术学院软件基础教研室 第二部分 Android 与工程和科研 授课教师:李治军 综合楼 411 室 第 5 讲 Android 人机交互与游戏 Lecture 5: Human-Computer Interface in Android and Games
- 2 - Android Development 人机交互是计算机发展的动力之一 打孔纸带 键盘 ( 命令行 ) 鼠标 ( 图形控件 ) … 对于手机来说,用手点按钮也不方便,屏幕太小 (1) 获得加速度 raw data ; (2) 建立数学模型 ( 物理、几 何 ) ; (3) 计算手臂动作; (4) 推测人的意图; (5) 根据意 图对机器进行控制; (6) 做出实际应用 可以对着手机说话,也挺费劲 … 可以做手势 因为手机有加速度传感器器,可以识别手臂活动 观察世界,形成想法,然后实现这些想法
- 3 - Android Development 得到加速度的原始数据 仍然是 Android 的广播、接收广播结构 很多传感器类型 : TYPE_LIGHT , TYPE_PRESSURE , TYPE_TEMPERATURE… SensorManager sm = getSystemService(Context.SENSOR_SERVICE); int sensorType = Sensor.TYPE_ACCELEROMETER; sm.registerListener(myAcceListener,sm.getDefau ltSensor(sensorType),SensorManager.SENSOR_DELA Y_NORMAL); 注册侦听的函数有三个参数 : 侦听程序,侦听的传感器 ( 每种类型的传感器可能有多个,所以有缺省 ) ,设置侦 听的频率 ( 多长时间发送一次数据给侦听程序 )
- 4 - Android Development 开始写侦听程序 Android 专门定义了 SensorEventListener() 只要重载其中 onSensorChanged( 有数据来 ) 方法即可 SensorEventListener myAcceListener = new SensorEventListener(){ public void onSensorChanged(SensorEvent event){ if(event.sensor.getType() == Sensor. TYPE_ACCELEROMETER){ float X_lateral = event.values[0]; float Y_longitudinal = event.values[1]; float Z_vertical = event.values[2]; 只要将这三个轴的加速度保存在数组中就可以处理
- 5 - Android Development 有了加速度原数据以后需要进行建模 就是得到一个加速度数据序列对应的 “ 动作 ” 首先画出一个序列看一看 看出什么了 ? X,Y 变化较小, Z 有明 显的变化,说明在竖 直方向上有晃动
- 6 - Android Development 就是在竖直方向上晃动 — 我想翻 PPT 开始建模 : 用一点物理知识 … 上 ( 下 ) 翻 PPT ,就是给一个短时间向上 ( 下 ) 的力量 F=ma ,所以就是 “ 短时间内向上 ( 下 ) 的加速度 ” (1) 短时间:采样可 以记录时间, W < T 这些地方就很像 (2) 有力量:加速度 累加值大, a(W) > A W 是 一个 时间 窗口 (3) 判断方向:重力 加速度 g 总向下, a 和 g 的方向
- 7 - Android Development 我们说的力量应该滤掉重力 右边的图应该是包含了重力 TYPE_LINEAR_ACCELERATION sensorType = Sensor.TYPE_LINEAR_ACCELERATION; SDK 版本 2.3 以上 应该是有一个向上的 力 让手抬起 应该紧接着一个向下 的力 让手停下 v = ad = v
- 8 - Android Development 有了模型就可以给出算法了 思想 : (1) 找到一个窗口 W( 从 0 开始累加直到累加 为 0) ,即 W a = 0 ; (2) 判断这个窗口是否是短促 的发力,即 W |a|/|W| > ; (3) 判断 W 时间内的手 臂旋转角度 while(AccInt >= 0){ AccInt = AccInt + AccZ[i]; i = i+1;} EndWindow = i; d = v for(i = StartWindow; i <= EndWindow; i++) AccPower = AccPower + abs(AccZ[i]); if AccPower / (EndWindow - StartWindow) then …
- 9 - Android Development 根据重力来判断方向 对于 Z 轴,接近 9 的重力应该是手机平放,加速度 为 0 应该是手机竖直放,此时 Y 轴的值应该为 9 sensorType = Sensor.TYPE_TYPE_GRAVITY; SDK 版本 2.3 以上 Angle = arcsin((GravityZ[StartW indow] – GravityZ[EndWindow])/9); if Angle >= 80 then 发 出向上的指令
Android Development 开始用手机控制笔记本上的 PPT 就是用网络通信给笔记本发送键盘动作 首先要解决的问题是手机和笔记本进行通信 选择通信手段 : 3G 、 Wifi 、蓝牙 Wifi 选择通信手段 : 3G 、 Wifi 、蓝牙 此处让笔记本成为 Wifi 热点 热点 (AP) (1) AP 每 100ms 广播 SSID (2) Client 收到 SSID 以后,决定是否连接 (3) 发起连接,进行 WiFi 认证,通过后完成连接的建立 (4) 一旦建立 WiFi 连接,上层的数据 (TCP/IP 层 ) 就会 用这个连接 (MAC 层、物理层 ) 进行封装发送 著名的
Android Development 列出周围开启 PPT 控制服务的 Wifi 热点 列出周围的 AP 已经搞定,用程序询问是否服务 用程序连接这些 AP( 当然密码已经记录下来 ) WifiConfiguration wCfig = new WifiConfiguration(); wCfig.SSID = "\"lizhijun\""; wCfig.preSharedKey = "\" \""; wCfig.allowedKeyManagement.set(WifiConfiguration. KeyMgmt.WPA_PSK); int nWifiID = mWifiMgr.addNetwork(wCfig); boolean bWifiCon = mWifiMgr.enableNetwork(nWifiID, true); 询问某个端口 (“7777”) ,看是否收到 Hello?
Android Development 现在可以向 7777 发起询问了 就是发起数据通信,当然就是 socket 一个 socket 应该包含 IP 地址和端口 mSocket = new Socket(ServerIP, 7777); ServerSocket srvsock = new ServerSocket(7777); while (true) { Socket sock = srvsock.accept(); 这就建立一个手机到笔记本的 TCP 连接 ( 传输控制命令 ) IP 地址应该是什么 ? 本机 IP 地址是什么 ? DHCP IP 地址应该是什么 ? 当然此处的 IP 地址是服务器的 IP ,即笔记本的 服务器要侦听这个端口 Android 网络编 程也一样
Android Development 现在关键问题是如何获得服务器 IP 可以在程序中往里敲,但是这样也太笨了 手机发起扫描周围启动 PPT 控制服务的服务器 DatagramSocket socket = new DatagramSocket(); String strPptQuery="whoispptctrl"; DatagramPacket packet = new DatagramPacket(strPptQuery.getBytes(),strPptQu ery.length(),InetAddress.getByName(" "),7778); socket.send(packet); (1) 手机广播 UDP 包,内容是 “whoispptctrl” 手机发起扫描周围启动 PPT 控制服务的服务器 (2) 服务器启动一个线程,收到上述内容后回答 “iampptctrl” 并且告诉服务器的 IP 地址
Android Development 服务器端启动线程接收 UDP 询问 定义一 implements Runnable 类,放入 Thread 每当线程调度时就会执行该 runnable 类中的 run 函数 ReceiveHelloFromPhone udpReceive = new ReceiveHelloFromPhone(); Thread udpReceiver = new Thread(udpReceive); udpReceiver.start(); DatagramSocket server=new DatagramSocket(7778); DatagramPacket packet=new DatagramPacket(buffer,); while(true){ server.receive(packet); String content = new String(packet.getData(),); InetAddress clientAddr = packet.getAddress(); if(content.equals("whoispptctrl"))
Android Development 手机在收到服务器 IP 后发起 TCP 连接 一旦建立连接就象管道 : sock.getInputStream() 直接从这个管道中 read 和 write fromClient = new ObjectInputStream(sock.getInputStream()); fromServer = new ObjectOutputStream(sock.getOutputStream()); Command cmd = (Command)fromClient.readObject(); action = cmd.getAction(); if(action == DOWN) 产生键盘动作 产生键盘动作用的是 Java 的 Robot 类,这个类可以自 动产生你的各种操作
Android Development 所以 PPT 控制就成为 手机用加速度传感器 track 用户动作;在手机上 用一点 AI 的知识和算法识别上 ( 下 ) 翻动作;手机 把这个动作通过 Wifi 发给笔记本;笔记本上的服 务器收到命令以后用 Robot 产生一个键盘指令 Command cmd = new Command(DOWN); try { fromClient.writeObject(cmd); if(action == DOWN) robot.keyPress(KeyEvent.VK_DOWN); Thread.sleep(10); robot.keyRelease(KeyEvent.VK_DOWN); 手机发送命令 Robot 产生指令
Android Development 接下来编写一个用传感器控制的小游戏 一个 3D 小球在手机屏幕上 ( 按斜面原理 ) 滚来滚去 (1) 画出 3D 游戏场景,用 openGL public GameMainView mainView; onCreate() {mainView = new GameMainView(this); setContentView(mainView); (2) 根据手机姿态计算重力在屏幕斜面上分量 (3) 让小球移动起来 首先开始 openGL (open Graphics Library) 在 Activity 中 class GameMainView extends GLSurfaceView{ public GameMainView(Context context) {
Android Development 开始 OpenGL 的渲染工作 (3D 全靠渲染 ) 依靠一个 implements Render 的类来实现 其中有个方法是 onSurfaceCreated ,画布新建时调用 class GameMainView extends GLSurfaceView{ public GameMainRender mainRender; public GameMainView(Context context) { mainRender = new GameMainRender(); setRenderer(mainRender); } class GameMainRender implements Renderer{ onSurfaceCreated(GL10 gl, EGLConfig arg1) { 在画布新建时把 3D 小球初始化画出来吗 ? 其中有个方法是 onSurfaceCreated ,画布新建时调用
Android Development 开始画一个小球 实际上 Surface 创建时做初始化 ( 如背景颜色 ) 真正用来画一个实体的函数在 onDrawFrame 中 onSurfaceCreated(GL10 gl, EGLConfig arg1) { gl.glClearColor(0,0,0,0); public void onDrawFrame(GL10 gl) { Ball ball = new Ball(); initScene(gl); ball.draw(gl); 开始创建 3D 小球 gl.glVertexPointer(3,.., 0, vertexBuffer); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, VertexNumber); 在 draw 中 开始创建 3D 小球
Android Development 3D 对象创建实际上就是创建一堆点 for(int v = -90; v <= 90; v = v + step) { for(int h = 0; h <= 360; h = h + step) { r1 = Math.cos(v * Math.PI / 180.0); h1 = Math.sin(v * Math.PI / 180.0); r2 = Math.cos((v + step) * Math.PI / 180.0); h2 = Math.sin((v + step) * Math.PI / 180.0); co = Math.cos(h * Math.PI / 180.0); si = -Math.sin(h * Math.PI / 180.0); data[m][0] = r2 * co; data[m][1] = h2; data[m][2] = r2 * si; data[m+1][0] = r1 * co; data[m+1][1] = h1; data[m+1][2] = r1 * si; 在球面上 一顿算
Android Development 3D 渲染效果表现为很多平面的填充 for(int i=0; i < nVertexCount; i++) { vertexBuffer.put(data[i]); } vertexBuffer.position(0); gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); gl.glNormalPointer(GL10.GL_FLOAT, 0, vertexBuffer); gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, nVertexCount); 一堆球面上的三角形, 填充后就 3D 了 initScene 用来创建光源 gl.glMaterialf(GL10.GL_FRONT_AND_ BACK, GL10.GL_SHININESS, 64.0f); 0.0f
Android Development 现在让小球滚动起来 就是旋转加上位移 首先实现旋转,一句基本函数 gl.glRotatef(angle, 0.0f, 1.0f, 0.0f); data[m][0] = r2 * co + 1f; data[m][1] = h2 + 2f; 然后实现位置移动:很简单,修改坐标即可 就会往右上角移动了 用传感器控制小球的运动 : 姿态传感器 sm = getSystemService(Context.SENSOR_SERVICE); int sensorType = Sensor.TYPE_ORIENTATION; sm.registerListener(mySensorListener,...); 旋转角度 + 旋转轴
Android Development 关于姿态传感器 Yaw( 偏航 ),Pitch( 俯仰 ),Roll( 旋转 ) TYPE_ORIENTATION 不好使,方式已过期 sensorType = Sensor.TYPE_ACCELEROMETER; sensorType = Sensor.TYPE_MAGNETIC_FIELD; 实际上 Android 的姿态是用磁力计和加速度算出来的 if(event.sensor.getType()==TYPE_MAGNETIC_FIELD) magneticFieldValues = event.values; if(event.sensor.getType()==TYPE_ACCELEROMETER) accelerometerValues = event.values; controlByOrientation(); 然后注册 Listener 在 onSensorChanged 中
Android Development 获得俯仰角和旋转角并控制小球 用 getRotationMatrix 得到旋转数组,用来存放 磁场和加速度的数据,获得姿态时要用 float[] values = new float[3]; float[] R = new float[9]; sm.getRotationMatrix(R, null, accelerometerValues, magneticFieldValues); sm.getOrientation(R, values); 根据 P 角决定 Y 轴上的移动量, … 存放姿态数据,依 次是 Y,P,R V x (t) = V x (t-1) + G x (t-1) t S x (t) = V x (t-1) t + G x (t-1) t 2 /2 G x (t) = G sin(R) G = 9.8 t = 1 onDrawFrame 会时 调用,要 gl.glClear (GL10.GL_COLOR_ BUFFER_BIT);