在上节实现了手柄输入之后,提到过这节将会把两种输入方式的共通点抽象出来,写成一个抽象基类,而两种输入方式成为其的子类。
首先我们可以来找找两种输入方式有什么共通点。在人物移动上,key string name两者对应的按钮不一样,肯定不用考虑。在移动信息处理上,虽然两者的处理方式不一样,但都需要变量来存储相关信息,可以把变量提取出来。这些都是两者都有的变量:
[Header("===== Move =====")]
private float targetUp; //键入按钮直接转1跟0
private float targetRight;
private float curVelocityDup; //平滑处理函数要用到的引用参数
private float curVelocityDright;
public float Dup; //平滑处理
public float Dright;
public float Dmag; //记录移动速度
public Vector3 Dvec; //记录移动方向
在相机移动上,也有类似的变量是两者都有的:
[Header("===== Camera Move =====")]
public float CUp; //相机移动方向
public float CRight;
还有3个动作的信号(跑、跳、滚)也是两者都有的变量:
[Header("===== State =====")]
public bool run;
public bool jump;
public bool attack;
最后我们之前说过,两者都是需要椭圆映射法来解决斜向1.414的问题,所以都需要我们自己定义的SquareToCircle(Vector2 temp)
函数:
Vector2 SquareToCircle(Vector2 temp){
Vector2 output = Vector2.zero;
output.x = temp.x * Mathf.Sqrt (1 - (temp.y * temp.y) / 2);
output.y = temp.y * Mathf.Sqrt (1 - (temp.x * temp.x) / 2);
return output;
}
至此两者的共同点都基本找完了,其实都是两者都会用到的变量和函数。现在需要我们去新建一个抽象类,把这些通通都灌进去了。
在这里我想先讨论一下为什么选择抽象类而不是接口。接口里面是不允许定义函数,只允许声明函数,实现部分是留给子类去解决的;而抽象类则允许定义函数在里面。因为现在这两个输入方式用到的椭圆映射法的函数是同一实现,并无区别的,所以使用抽象类更好一点。
在project视窗,右键create→C# Script新建C#脚本文件,命名为IUserInput(前缀为I表明它是一个抽象类or接口):把刚才列出来的代码通通塞进去:
public abstract class IUserInput : MonoBehaviour {
public bool InputEnabled=true;
[Header("===== Move =====")]
public float Dup;
public float Dright;
protected float curVelocityDup;
protected float curVelocityDright;
protected float targetUp;
protected float targetRight;
public float Dmag;
public Vector3 Dvec;
[Header("===== Camera Move =====")]
public float CUp;
public float CRight;
[Header("===== State =====")]
public bool run;
public bool jump;
public bool attack;
protected Vector2 SquareToCircle(Vector2 temp){
Vector2 output = Vector2.zero;
output.x = temp.x * Mathf.Sqrt (1 - (temp.y * temp.y) / 2);
output.y = temp.y * Mathf.Sqrt (1 - (temp.x * temp.x) / 2);
return output;
}
}
要注意的是,为了能让子类用上父类的变量和函数,要把原本修饰符是private的变量和函数改为protected,这样这些就只能子类去使用了,其他类是用不了的。基类就这样完事了,现在要考虑的就是两个子类和涉及到这两个子类的其他组件的修改。
把PlayerInout和JoystrickInput的父类改为IUserInput,并把父类已经有的东西给删除掉。
public class JoystickInput : IUserInput
和public class PlayerInput : IUserInput
。诶有人可能会问:这里继承了IUserInput,那原本它们继承的MonoBehaviour现在没得继承了,岂不是很多函数都用不了了?非也,因为它们的父类IUserInput已经继承了MonoBehaviour了,所以它们自己也会继承MonoBehaviour的。
现在要对一些涉及了输入类型变量的组件进行修改,在这里就有两个:ActorController.cs和CameraController.cs,在没有实现手柄输入前,这两个都声明了PlayerInput类型的变量pi,用来获取输入的相关信息。现在要把它们改为IUserInput:
public IUserInput pi;
在获取组件方面,不能用GetComponent<>()
,因为对于IUserInput,其真正实现是两个子类,如果想实现两种输入方式都检测得到的话,要用GetComponents<>()
,否则Unity只会按组件顺序优先取第一个组件,无论其是否被勾上,像这样:
如果你用的是
GetComponent<>()
,那么无论你PlayerInput是否勾上,它都只会获取这个组件而忽略JoystickInput。所以我们应该用GetComponents<>()
获取一个组件数组,然后逐个侦测看哪个组件被勾上,就选取该组件作为输入方式。在ActorController.cs里,我们是使用
enabled
属性去获知哪个输入方式是被勾上的:
void Awake(){
IUserInput[] temp = GetComponents<IUserInput>();
foreach (var item in temp){
if (item.enabled == true) {
pi = item;
}
}
...
}
而对于CameraController.cs,之前获取PlayerInput的做法是把这个变量曝露出去,然后在外面把PlayerInput组件直接灌进来。这次我通过原本已有的PlayerHandle变量去获得IUserInput,因为在PlayerInpupt的ActorController.cs里面(如上)已经为我们选好了能用的输入方式,所以在这直接把它拿过来用就是了,没必要再来筛选一遍了。
void Awake () {
cameraHandle = transform.parent.gameObject; //负责垂直旋转
playerHandle = cameraHandle.transform.parent.gameObject; //负责水平旋转
//pi = playerHandle.GetComponent<ActorController>().pi;
model = playerHandle.GetComponent<ActorController>().model; //获取模型
camera = Camera.main.gameObject; //获取主要摄像头(Tag为MainCamera)
tempEuler = 20.0f;
}
void Start(){
pi = playerHandle.GetComponent<ActorController> ().pi;
}
不过要注意的是,这里pi的初始化是不能放在Awake()
阶段的,因为pi初始化所需要的playerHandle变量也是在Awake()
阶段才被初始化,所以是要到它被初始化后(Awake()
函数执行完毕),才能去初始化pi。因此要把它的初始化放在Start()
阶段。
至此,整个抽象类别就完成了。现在可以来看看效果:
可以看到现在是用手柄输入的,且各项功能正常。下一节我们将开始讨论防御的实现。