背景:程序猿小王在项目中写了一个相机控制模块,通过键盘和鼠标达到第一人称和第三人称相机操控功能。Ok,做完了,很嗨皮。突然有一天。
- 策划A :小王,日本有个版本需要用GearVR来操作,你改一下吧;
- 小王 :Ok,没问题,看我加个设备驱动,然后ifelse就搞定了;
- 策划B :小王,香港那边有个版本需要用Htc Vive,要用手柄控制你这边改一改吧;
- 小王 :你保证这是最后一次?好吧,我再加个设备驱动,然后再else就搞定了;
- 策划C :小王,这个操控能在普罗米修斯的白板上用吗?
- 小王 :%¥…1………
- 主程:小王,你把相机控制模块抽取出来变成一个插件吧!
小王心想 :这虽然不是什么大问题,如果变成一个单独的dll,可能需要依赖下GearVR和Htc的设备驱动,要是以后策划再“找麻烦”,那岂不是要不停的改动这个动态库,依赖越来越多的dll,越来越多的switchcase语句。有什么方式可以做到满足“开闭原则”,即对于扩展是开放的,对于修改是关闭的。并且这个模块不依赖于任何的第三方设备驱动,毕竟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<AxisConfig> axisconfigs;//初始化后获得所有轴的定义
//增加自定义外设支持
public void AddBindingTarget<T>() 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<T>() where T : TargetElem,new()
{
axisconfigs.ForEach(axisconfig =>
{
axisconfig.axisBinding.RemoveTarget<T>();
});
}
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<TargetElem4GearVR>();