游戏开发论坛

 找回密码
 立即注册
搜索
查看: 5868|回复: 0

手机游戏无障碍设计——猜地鼠之Android篇

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2017-2-6 16:03:29 | 显示全部楼层 |阅读模式
  作者简介:何金源,腾讯Android手Q开发工程师,2011年本科毕业,负责Android手Q无障碍优化工作,对Android无障碍系统原理及开发技术有深入了解。

  手机应用无障碍化逐渐受到重视,这项技术为盲人或者视力有障碍的人士带来了很大便利。那么对于手机游戏,同样也应该进行无障碍化,本文将以盲人猜地鼠游戏为例,讲解如何对手机游戏进行无障碍化设计,如何让原本无法操作变成可在无障碍模式下正常使用,最后总结手机游戏无障碍化的大体思路。

  前言

  目前市场上针对盲人进行无障碍化的手机游戏几乎没有,这对于障碍用户来讲,是一大遗憾。实际上,他们对游戏的渴望跟对应用无障碍化的渴望一样强烈,他们也希望能在手机上体验一把游戏的乐趣。下面,我们来探讨手机游戏无障碍化。

  由于是首次针对手机游戏进行无障碍化,所以这里挑选了一款较为简单的游戏——猜地鼠。猜地鼠是基于MasterMind游戏的玩法(如图1)。原本玩法是两个人对玩,其中一人是出谜者,摆好不同颜色的球的位置;另一个人是猜谜者,每个回合猜各个位置应该放什么颜色的球。每回合结束后会有结果提示。

1.jpg
图1 MasterMind游戏

  而猜地鼠则是会有不同颜色的地鼠,用户每个回合要猜地鼠的颜色排列顺序。这是使用Cocos2d-x引擎开发的,引擎并没对无障碍模式做出优化,所以游戏开发完成后只有一个大焦点在界面上,当用户点击这个焦点时,没法进行下一步操作,怎么点也进不了游戏界面。如图2所示。

2.jpg
图2 优化前的游戏界面

  构造无障碍虚拟节点

  Cocos2d-x引擎是支持跨平台的,所以我们可以针对不同平台区分处理无障碍。首先看下Android平台,在游戏中,Cocos2d-x是把游戏里的界面、图片和文字等素材画到GLSurfaceView上,而GLSurfaceView是添加到Cocos2dxActivity的布局中。Cocos2dxActivity是Cocos2d-x引擎在Android平台上最主要的Activity,我们要对游戏进行无障碍化,就需要让这个Activity支持无障碍。

  在Android平台,可以对自定义View进行无障碍化支持。具体的原理可以参考官网上的介绍,或者《程序员》8月发布的《Android无障碍宝典》。所以我们第一步,就是为Cocos2dxActivity增加一个自定义View。

  1. public class MasterMind extends Cocos2dxActivity{

  2.     private AccessibilityGameView mGameView;

  3.     protected void onCreate(Bundle savedInstanceState){
  4.         super.onCreate(savedInstanceState);

  5.         Rect rectangle = new Rect();
  6.         getWindow().getDecorView().getWindowVisibleDisplayFrame(rectangle);
  7.         AccessibilityHelper.setScreen(rectangle.width(), rectangle.height());

  8.         @SuppressWarnings("deprecation")
  9.         LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
  10.                 getWindowManager().getDefaultDisplay().getWidth(),
  11.                 getWindowManager().getDefaultDisplay().getHeight());
  12.         mGameView = new AccessibilityGameView(this);
  13.         addContentView(mGameView, params);

  14.         AccessibilityHelper.setGameView(mGameView);

  15.         ViewCompat.setImportantForAccessibility(mGLSurfaceView, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO);
  16.     }

  17.     static {
  18.          System.loadLibrary("game");
  19.     }
  20. }
复制代码

  AccessibilityGameView是个简单透明的自定义View,直接通过addContentView方法加入到布局中,同时把GLSurfaceView设置为不需无障碍焦点。

  1. public class AccessibilityGameView extends View {

  2.     private BaseSceneHelper mTouchHelper;

  3.     public AccessibilityGameView(Context context) {
  4.         super(context);
  5.     }

  6.     public void setCurSceneHelper(BaseSceneHelper helper){
  7.         mTouchHelper = helper;
  8.     }

  9.     @SuppressLint("NewApi")
  10.     @Override
  11.     protected boolean dispatchHoverEvent(MotionEvent event) {
  12.         if(mTouchHelper != null && mTouchHelper.dispatchHoverEvent(event)){
  13.             return true;
  14.         }

  15.         return super.dispatchHoverEvent(event);
  16.     }

  17. }
复制代码

  AccessibilityGameView中包含BaseSceneHelper类,并将HoverEvent交给BaseSceneHelper处理。BaseSceneHelper负责构造自定义无障碍虚拟节点,提供游戏中必要信息给用户。首先看BaseSceneHelper内部的实现。

  1. public class BaseSceneHelper extends ExploreByTouchHelper {
  2.     protected ArrayList<AccessibilityItem> mNodeItems;
  3.     public BaseSceneHelper(View forView) {
  4.         super(forView);
  5.         mNodeItems = new ArrayList<AccessibilityItem>();
  6.     }
  7.     @Override
  8.     protected int getVirtualViewAt(float x, float y) {
  9.         for (int i = 0; i < mNodeItems.size(); i++) {
  10.             Rect rect = mNodeItems.get(i).mRect;
  11.             if (rect.contains((int) x, (int) y)) {
  12.                 return i;
  13.             }
  14.         }
  15.         return -1;
  16.     }
  17.     @Override
  18.     protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
  19.         for (int i = 0; i < mNodeItems.size(); i++) {
  20.             virtualViewIds.add(mNodeItems.get(i).id);
  21.         }
  22.     }
  23.     @Override
  24.     protected void onPopulateNodeForVirtualView(int virtualViewId,
  25.             AccessibilityNodeInfoCompat node) {
  26.         if(mNodeItems.size() > virtualViewId){
  27.             node.setContentDescription(getContentDesc(virtualViewId));
  28.             setParentRectFor(virtualViewId, node);
  29.         }
  30.     }
  31.     private String getContentDesc(int virtualViewId) {
  32.         if (mNodeItems.size() > virtualViewId) {
  33.             return mNodeItems.get(virtualViewId).mDesc;
  34.         }
  35.         return "";
  36.     }

  37.     …..
  38. }
复制代码

  BaseSceneHelper实现无障碍辅助类ExploreBy TouchHelper,通过维护AccessibilityItem列表,将所需要的无障碍虚拟节点的Rect和Description记录起来。当BaseSceneHelper被调用创建无障碍节点时,实时提供AccessibilityItem。

  1. public class AccessibilityItem {

  2.     public Rect mRect;
  3.     public String mDesc;
  4.     public int id;
  5. }
复制代码

  处理游戏场景切换

  到此,就完成了Java层的无障碍化,但是游戏中需要的无障碍焦点,得从游戏代码中触发。对猜地鼠进行无障碍化,一共要处理4个场景。一进入游戏就能看到的菜单场景(如图3),会有两个按钮需要无障碍化;从开始游戏按钮,我们会进入游戏场景(如图4),这个场景相对比较复杂,需要对每个不同颜色的地鼠进行无障碍化,然后需要对当前回合的结果提示进行无障碍化,还要提供一个结束游戏的按钮;游戏共9个回合,如果都猜不中则是输了,如果猜中则赢得游戏,两种情况都会弹出游戏结束场景(如图5)。这个场景需要告诉用户最终结果是什么,花费多长时间,以及提供一个重新开始游戏的操作。最后一个场景是帮助场景(如图6),它其中只需要告诉用户这个游戏的玩法,以及提供返回菜单场景的操作就行。
3.jpg
菜单场景

4.jpg
图4 游戏场景

5.jpg  
图5 结束场景

6.jpg  
图6 帮助场景

  接着来看下如何对场景无障碍化,举个例子,对菜单场景进行无障碍化。首先,需要去掉原来的大焦点;接着,为开始游戏按钮和怎么玩按钮提供无障碍焦点和无障碍信息,也就是说,要构造两个无障碍节点。对AccessibilityGameView添加两个无障碍虚拟节点。这样,用户就能操作到场景中这两个按钮。

  进入到游戏代码中,两个按钮是CCMenuItemLabel控件,通过调用rect()方法,可以获得两个按钮在屏幕中的大小。

  1. m_pItemMenu = CCMenu::create();
  2.     for (int i = 0; i < menuCount; ++i) {
  3.         CCLabelTTF* label;
  4.         label = textAddOutline(menuNames[i].c_str(),
  5.                                        "fonts/akaDylan Plain.ttf", 30, ccWHITE, 1);
  6.         CCMenuItemLabel* pMenuItem = CCMenuItemLabel::create(label, this,
  7.         menu_selector(HelloWorld::menuCallback));
  8.         m_pItemMenu->addChild(pMenuItem, i + 10000);
  9.         pMenuItem->setPosition(
  10.                 ccp( VisibleRect::center().x, (VisibleRect::bottom().y + (menuCount - i) * LINE_SPACE) ));
  11.         CCRect rect = pMenuItem->rect();
  12.         const char * str = GameConstants::getMenuNodeDesc(i);
  13.         AccessibilityWrapper::getInstance()->addMenuSceneRect(i, str, rect.getMinX(),rect.getMaxX(),rect.getMinY(),rect.getMaxY());
  14.     }
复制代码

  在取得按钮的大小后,将按钮描述str和rect交给AccessibilityWrapper。AccessibilityWrapper将会通过JNI的方法调用Java层代码,通知BaseSceneHelper来构造无障碍虚拟节点。

  1. public class BaseSceneHelper extends ExploreByTouchHelper {

  2.     protected ArrayList<AccessibilityItem> mNodeItems;

  3.     ....

  4.     public void updateAccessibilityItem(int i, String desc){
  5.         if(mNodeItems.size() > i){
  6.             AccessibilityItem item = mNodeItems.get(i);
  7.             item.mDesc = desc;
  8.         }
  9.     }
  10.     public void addAccessibilityItem(AccessibilityItem item) {
  11.         mNodeItems.add(item);
  12.     }
  13.     public void destroyScene() {
  14.         mNodeItems.clear();
  15.     }
  16. }
复制代码
  1. static AccessibilityWrapper * s_Instance = NULL;

  2. AccessibilityWrapper * AccessibilityWrapper::getInstance(){
  3.     if(s_Instance == NULL){
  4.         s_Instance = new AccessibilityWrapper();
  5.     }

  6.     return s_Instance;
  7. }

  8. void AccessibilityWrapper::addMenuSceneRect(int i, const char * s, float l, float r, float t, float b){
  9.     JniMethodInfo minfo;
  10.     bool isHave = JniHelper::getStaticMethodInfo(minfo,
  11.             "cn/robust/mastermind/AccessibilityHelper","addMenuSceneRect","(ILjava/lang/String;IIII)V");
  12.     if(!isHave){
  13.             //CCLog("jni:openURL 函数不存在");
  14.     }else{
  15.         int left = (int)l;
  16.         int right = (int)r;
  17.         int top = (int)t;
  18.         int bottom = (int) b;
  19.         jstring jstr = minfo.env->NewStringUTF(s);
  20.         minfo.env->CallStaticVoidMethod(minfo.classID,minfo.methodID, i, jstr, left, right, top, bottom);
  21.     }
  22. }
复制代码

  Java层被调用的类是AccessibilityHelper,在addMenuSceneRect方法中构造AccessibilityItem,放到菜单页面的BaseSceneHelper中。

  1. public class AccessibilityHelper {
  2.     private static BaseSceneHelper mMenuRef;
  3.     private static BaseSceneHelper mPlayRef;
  4.     private static BaseSceneHelper mOverRef;
  5.     private static BaseSceneHelper mHelpRef;
  6.     private static BaseSceneHelper mCurRef;
  7.     ……
  8. public static void addMenuSceneRect(int i, String d, int l, int r, int t, int b){
  9.         int sl = getScreenX(l);
  10.         int sr = getScreenX(r);
  11.         int st = getScreenY(b);
  12.         int sb = getScreenY(t);
  13.         AccessibilityItem item = new AccessibilityItem(i, d, sl, sr, st, sb);
  14.         if(mMenuRef != null){
  15.             mMenuRef.addAccessibilityItem(item);
  16.         }
  17.     }
  18. }
复制代码

  游戏跟应用的无障碍化不同,它只有一个activity,场景切换并不会改变activity。所以需要在上个场景结束时,把上个场景的无障碍虚拟节点删除,在下个场景出现之前,把下个场景的无障碍虚拟节点构造好。具体代码如下:

  1. public static void onMenuSceneLoad(int scene){
  2.         if(mGameViewRef.get() != null && mMenuRef != null &&
  3.                 mPlayRef != null && mOverRef != null){
  4.             switch (scene) {
  5.             case 0:
  6.                 ViewCompat.setAccessibilityDelegate(mGameViewRef.get(), mMenuRef);
  7.                 handleNewScene(mMenuRef);
  8.                 break;
  9.             case 1:
  10.                 ViewCompat.setAccessibilityDelegate(mGameViewRef.get(), mPlayRef);
  11.                 handleNewScene(mPlayRef);
  12.                 break;
  13.             case 2:
  14.                 ViewCompat.setAccessibilityDelegate(mGameViewRef.get(), mOverRef);
  15.                 handleNewScene(mOverRef);
  16.                 break;
  17.             case 3:
  18.                 ViewCompat.setAccessibilityDelegate(mGameViewRef.get(), mHelpRef);
  19.                 handleNewScene(mHelpRef);
  20.             }
  21.         }
  22.     }

  23.     private static void handleNewScene(BaseSceneHelper newScene){
  24.         if(mCurRef != null ){
  25.             mCurRef.destroyScene();
  26.         }
  27.         mCurRef = newScene;
  28.         mGameViewRef.get().setCurSceneHelper(newScene);

  29.     }
复制代码

  游戏引擎的特殊处理

  前面讲到的构造无障碍节点和处理场景切换,这两个流程都是从游戏代码中发起。而Cocos2d-x引擎中游戏代码是C++写的,所以这里通过jni技术,在C++代码中调用Android工程的Java代码,把构造无障碍节点和处理场景切换两件事告诉上层,并展示到UI上(如图7)。

7.jpg  
图7 引擎无障碍化通讯流程

  然而,在游戏世界中的坐标系与在手机中坐标系有些区别,如图8所示,在游戏中坐标原点是在左下角,而在手机屏幕中坐标原点在左上角。所以按钮的边框R1要转换成边框R2,需要对左上角坐标点和右下角坐标点进行变换。

8.jpg  
图8 坐标转换图

  比如左上角的点,在游戏中是(x1, y1),y1的值是红线段的长度。在右边,左上角的(x2,y2),y2的值是蓝色线段的长度。x2的值通过图8中的公式求出,其中SW是手机屏幕的宽度,而游戏场景设定屏幕大小为480 * 720。

  要求y2的值,先算出RH的值,RH是游戏场景在手机屏幕中的高度。由于游戏是宽度适应,手机上下会有黑边,RH是手机屏幕高度SH减去黑边BH,然后就可以根据公式求出y2的值。在代码中转换方法如下:

  1. private static int getScreenX(int x){
  2.         int ret = x * sWidth / sGameWith;
  3.         return ret;
  4.     }

  5.     private static int getScreenY(int y){
  6.         // 针对不同的拉伸方式要有不同的转换,这里是kResolutionShowAll
  7.         int realHeight = sGameHeight * sWidth / sGameWith;
  8.         int blackHeight = (sHeight - realHeight) / 2;
  9.         int y1 = sGameHeight - y;
  10.         int ret = y1 * realHeight / sGameHeight + blackHeight;
  11.         return ret;
  12.     }
复制代码

  经过坐标转换后,屏幕上的无障碍焦点就能完美的盖在菜单按钮上。当无障碍用户双击操作则会触发菜单按钮的点击操作。当用户触摸到菜单按钮位置时,则菜单按钮获取无障碍焦点。

  总结

  从猜地鼠游戏无障碍化看出,手机游戏要实现无障碍化并非难事。首先要为游戏界面添加自定义View,并且对这个自定义View进行无障碍化,使得它具有构造出虚拟无障碍节点的能力。然后,就要在游戏代码中计算出要展示给用户的无障碍节点的边框大小,经过坐标的转换,计算出在手机屏幕中展示的边框大小。将边框大小与描述信息,通过JNI层,通知到界面。以此类推,在各个场景中都进行无障碍焦点边框的处理,并且在切换场景时,把上个场景的无障碍焦点都清除掉。每个场景中,随着游戏操作变化,界面上的元素也可能发生变化,这时只要动态的维护场景的无障碍焦点,则用户就能感知游戏的变化。

  当然,这种实现方式,目前仅支持变化不多、比较简单的游戏(比如像猜地鼠这样的智力游戏),对于其他类型的游戏,可能需要更多的无障碍化实现,希望这个例子能给予游戏开发者解决灵感,为更多的游戏实现无障碍化。

via:《程序员》

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-4-26 15:44

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表