一种可灵活扩展外设的操作模块的实现方式

背景:程序猿小王在项目中写了一个相机控制模块,通过键盘和鼠标达到第一人称和第三人称相机操控功能。Ok,做完了,很嗨皮。突然有一天。

  • 策划A :小王,日本有个版本需要用GearVR来操作,你改一下吧;
  • 小王 :Ok,没问题,看我加个设备驱动,然后if­else就搞定了;
  • 策划B :小王,香港那边有个版本需要用Htc Vive,要用手柄控制你这边改一改吧;
  • 小王 :你保证这是最后一次?好吧,我再加个设备驱动,然后再else就搞定了;
  • 策划C :小王,这个操控能在普罗米修斯的白板上用吗?
  • 小王 :%¥…1………
  • 主程:小王,你把相机控制模块抽取出来变成一个插件吧!

小王心想 :这虽然不是什么大问题,如果变成一个单独的dll,可能需要依赖下GearVR和Htc的设备驱动,要是以后策划再“找麻烦”,那岂不是要不停的改动这个动态库,依赖越来越多的dll,越来越多的switch­case语句。有什么方式可以做到满足“开闭原则”,即对于扩展是开放的,对于修改是关闭的。并且这个模块不依赖于任何的第三方设备驱动,毕竟VR发展这么快,新设备会不停的冒出来。

原来的实现方式

    Vector3 moveDirection = Vector3.zero;
    #if UNITY_STANDALONE_WIN
        moveDirection = ...;//使用Unity的Input类方法
    #endif
    #if UNITY_GEARVR
        moveDirection = ...;//使用Oculus的OVRInput类方法
    #endif
    #if UNITY_HTCVR
        moveDirection = ...;//使用HTC的类方法
    #endif
    …… //使用moveDirection控制相机运动

这里使用宏控制执行逻辑,打包不同设备的包的时候,通过编译宏设置控制逻辑走向。
缺点1:动态库需要随着设备的增加而更新。
缺点2:动态库需要依赖除了UnityEngine外的其他设备驱动dll

理想的使用方式

  • 相机控制动态库只依赖UnityEngine
  • 可以很方便的扩展第三方外设驱动
  • 可以同时使用多种设备同时进行操作,并不互相冲突

总结下,例如要根据业务需要接入一款新的VR设备,那么首先需要在业务层依赖相机控制dll,以及VR设备的驱动dll;其次在业务层实现一套对接逻辑,告诉业务层使用新的设备来操控相机。至于怎么实现这套对接逻辑,需要仔细思考下。

实现方式的思考

参考Unity对输入的设计可以发现,他提供的都是原子级别颗粒的操作接口

    UnityEngine.Input.GetAxisRaw(axis);
    UnityEngine.Input.GetAxis(axis);
    UnityEngine.Input.GetButton(axis);
    UnityEngine.Input.GetButtonDown(axis);
    UnityEngine.Input.GetButtonUp(axis);

再看下,Oculus设备驱动的设计,也可以发现类似的风格:


这里写图片描述

最后看下,触屏外设控制插件ControlFrek2的源码,也可以发现类似的接口设计:


这里写图片描述

我们在设计的时候保证操作的颗粒级别跟UnityEngine提供的一致,而且外设的操作,确实也不外乎按下,按住,弹起等状态,涉及轴的操作返回浮点数。基于这点考虑,我们可以设计相机模块的控制类RPGInput

public static class RPGInput
{
    public static float GetAxis(string axisName)
    {
        //待实现
    } 
    public static float GetAxisRaw(string axisName)
    {
        //待实现
    } 
    public static bool GetButton(string axisName)
    {
        //待实现
    } 
    public static bool GetButtonDown(string axisName)
    {
        //待实现
    } 
    static public bool GetButtonUp(string axisName)
    {
        //待实现
    }
}

将原来调用 Input.GetXXX 的地方直接改为 RPGInput.GetXXX 。这样对原来相机控制库的代码改动量也最少。之后,需要有一个外设管理类来协调外设输入与定义。

外设输入定义与多种外设的关联

基于之前第三点的考虑(可以同时使用多种设备同时进行操作,并不互相冲突),当使用 RPGInput.GetXXX() ,会根据当前连接的所有外设输入值取一个合适的值(求和?)。也就意味着外设输入定义不仅仅是针对一种外设,而是可能跟多种设备有关联,关系图如下:

这里写图片描述

1、可以看到 外设输入定义 需要跟多个外设 关联,把这种关联关系定义为绑定 ,即1个输入定义需要绑定N个外设的某个按键或者操作。
2、当需要引入一种新的外设的时候,只要实现一个接口或者继承一个类,实现或者复写其中的方法,就可完成这种绑定关系。

那么这里假设需要几个类来实现绑定关系。

类功能表

类名 功能 例子
RPGAxisDefine 相机操作模块用到的轴定义类 RPGAxisDefine.MouseX对应原来代码中的 "MouseX"
AxisConfig 轴配置类,关联外设绑定类 与RPGAxisDefine 1对1
AxisBinding 外设绑定类,外设操作实现类的容器管理类 与AxisConfig1对1
TargetElem 外设操作实现类,对接具体外设的输入值 与TargetElem1对N

那么与上图对应后,各类所扮演的角色如图所示

这里写图片描述

具体的类图可以设计为:
这里写图片描述

代码走向:例如将要获取某个轴的输入值时,AxisConfig中的GetAxis()代码负责将AxisBinding中所有的外设绑定实现类TargetElem中的GetAxis()计算后,再做求和运算,得到最终值。
遗留问题:
1、这里只解决一个输入定义,即只有一个轴,需要一个类来管理所有轴
2、如何添加更多外设,即继承TargetElem后,需要一个接口来把该外设纳入绑定中

最终类图

为了解决上述遗留问题,需要再定一个类来管理所有轴的输入定义,并且可以增加/删除自定义外设;

public class RPGRig : MonoBehaviour
{
    public static RPGRig Instance;
    public List axisconfigs;//初始化后获得所有轴的定义
    //增加自定义外设支持
    public void AddBindingTarget() where T : TargetElem, new()
    {
        axisconfigs.ForEach(axisconfig =>
        {
            TargetElem instance = (TargetElem)Activator.Create
            Instance(typeof (T));
            axisconfig.axisBinding.AddTarget(instance).SetAxis(axisconfig.name);
        });
    } 
    //删除某种自定义外设支持
    public void RemoveBindingTarget() where T : TargetElem,new()
    {
        axisconfigs.ForEach(axisconfig =>
        {
            axisconfig.axisBinding.RemoveTarget();
        });
    }
    public float GetAxis(string axisName)
    {
        AxisConfig s = this.axisconfigs.Get(axisName);
        return ((s != null) ? s.GetAxis() : 0);
    } 
    …
}

其中GetAxis(string axisName)代码负责从所有轴定义中找到想要的轴配置实例axisConfig。
那么完整的类图就是如下图所示:

这里写图片描述

最终扩展使用方式

这里以GearVR为例,项目要求使用GearVR触控板前后滑动代替键盘WS前后移动功能。只需要2个步骤
1、继承TargetElem,并复写其中的方法

public class TargetElem4GearVR : TargetElem
{
    public const float SENSITY = 0.005f;
    public override float GetAxisRaw()
    {
        float value = 0.0f;
        if (axis.Equals(RPGAxisDefine.Vertical.Value))
        { 
            // 使用GearVR驱动方法返回值
            Vector2 primaryTouchpad = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad, OVRInput.Controller.Touchpad);
            var gearVRTouchPadX = primaryTouchpad.x;
            var gearVRTouchPadY = primaryTouchpad.y;
            Vector2 primaryRTRpad = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad, OVRInput.Controller.RTrackedRemote);
            var gearVRRTRX = primaryRTRpad.x;
            var gearVRRTRY = primaryRTRpad.y;
            Vector2 primaryLTRpad = OVRInput.Get(OVRInput.Axis2D.PrimaryTouchpad, OVRInput.Controller.LTrackedRemote);
            var gearVRLTRX = primaryLTRpad.x;
            var gearVRLTRY = primaryLTRpad.y;
            return (Mathf.Abs(gearVRTouchPadX) > SENSITY && Mathf.Abs(gearVRTouchPadY) < Mathf.Abs(gearVRTouchPadX) ?
                (gearVRTouchPadX > 0f ? 1f : -1f) : 0.0f) +
                (Mathf.Abs(gearVRRTRY) > SENSITY && Mathf.Abs(gearVRRTRX) < Mathf.Abs(gearVRRTRY) ? (gearVRRTRY > 0f ? 1f : -1f) : 0.0f) +
                (Mathf.Abs(gearVRLTRY) > SENSITY && Mathf.Abs(gearVRLTRX) < Mathf.Abs(gearVRLTRY) ? (gearVRRTRY > 0f ? 1f : -1f) : 0.0f);
        }
        ……
        return value;
    }
}

2、在相机控制模块初始化后,添加GearVR外设扩展绑定类

RPGRig.Instance.AddBindingTarget();

你可能感兴趣的:(一种可灵活扩展外设的操作模块的实现方式)