EasyCtrl,轻松搞定角色控制器输入!
大家有没有遇到过多平台的控制器需求,产品经理又要手机可以摇杆控制,又要求支持PC,突出一个折磨打工人。(冰冰和老菜喵合并啦!)
EasyCtrl是一个简单的角色控制器,同时CTRL也是“唱”,“跳”,“Rap”,“篮球”的简称。
通过EasyCtrl可以支持多种不同的控制输入,轻松控制角色唱跳RAP打篮球,同时在PC端,支持多按键Combo设置,和实时修改按键。
控制逻辑解耦
由于我们可能对接多个不同的控制器,如果在update里管理这些控制器会有大量的if else 不仅性能低,后期也不好维护。
这里二喵采用了中继的设计模式,以角色的位移为例子,把角色的位移分成了横向和纵向,这样一套控制器就可以适用于2D和3D不同的场景。
Vec3.copy(tempV3_0, this.node.position);
position.h = tempV3_0[_h];
position.v = tempV3_0[_v];
let dirV = ctrl.v, dirH = ctrl.h, move = false;
if (dirV != 0 || dirH != 0) {
const speedMag = speed * dt * ctrl.mag;
const eulerY = -Math.atan2(dirV, dirH) * rad + this.camera._targetRotation.y;
const targetRad = eulerY / rad;
this._speedH = lerp(this._speedH, speedMag * Math.sin(targetRad), dt * 5);
this._speedV = lerp(this._speedV, speedMag * Math.cos(targetRad), dt * 5);
quatFromAngleY(tempQ_0, eulerY);
move = true;
/* the maxStep should be angle instead of degree, already issued to engine team*/
Quat.rotateTowards(tempQ_0, this.node.rotation, tempQ_0, dt * 360 * 1.8);
position.h += this._speedH, position.v += this._speedV;
tempV3_0[_h] = position.h, tempV3_0[_v] = position.v;
this.node.rotation = tempQ_0;
this.play("walk");
} else {
this._speedH = this._speedV = 0;
this.play("idle");
}
if (move && !aa.ctrl.isPlaying) {
tempV3_0[_h] = position.h, tempV3_0[_v] = position.v;
this.node.position = tempV3_0;
}
其实角色控制器中横向和纵向的向量长度来自中继层InputCtrl,所以这里把InputCtrl 设计成了单例,不同的控制器就可以和角色控制器解耦合。
以虚拟摇杆控制器为例,我们先把虚拟摇杆位移的方向归一化。
touchMove(touch: Touch) {
if (!this.joyStick.active) return;
touch.getUILocation(_tempVec2);
this.joyStick.getWorldPosition(_tempVec3);
_tempVec2.x -= _tempVec3.x, _tempVec2.y -= _tempVec3.y;
this.distance = _tempVec2.lengthSqr();
this.distance = Math.min(this.distance,this.maxRadius);
_tempVec2.normalize();
ctrl.mag = (this.distance / this.maxRadius) * 1.2;
ctrl.h = _tempVec2.y, ctrl.v = _tempVec2.x;
_tempVec2.multiplyScalar(this.distance);
const euler = -Math.atan2(_tempVec2.x, _tempVec2.y) / rad;
this.indexArrow.rotation = Quat.fromAngleZ(_tempQuat,euler);
this.control.setPosition(_tempVec2.x, _tempVec2.y);
}
就可以把归一化后的横向和纵向的向量告诉InputCtrl,之后角色控制器不需要处理是哪个控制器在控制的,只需要读取横向和纵向的向量即可。
当我们得到了横向和竖向的向量长度,我们可以使用更节省的方法来结算euler,由于引擎只提供了quatFromAngleZ,这里补充了一个quatFromAngleY的方法,四元素的运算量会更少。
const halfToRad = 0.5 * Math.PI / 180.0;
export function quatFromAngleY<Out extends IQuatLike>(out: Out, y: number) {
y *= halfToRad;
out.x = out.z = 0;
out.y = Math.sin(y);
out.w = Math.cos(y);
return out;
}
在转向上,为了防止四元素锁死,这里使用 rotateTowards 的方法
/* the maxStep should be angle instead of degree, already issued to engine team*/
Quat.rotateTowards(tempQ_0, this.node.rotation, tempQ_0, dt * 360 * 1.8);
键盘控制器
对于键盘来说,由于我们需要组合按键,这里的逻辑就会复杂一点。
首先我们构建出按键事件,组合按键,按键绑定 这3个类型的数据结构。
export type KeyEvent = {
down?: Function
press?: Function
up?: Function
}
export type KeyCombo = KeyCode[];
export type KeyBinding = {
combo: KeyCombo,
event: KeyEvent,
label?: Label;
}
export type BindingConfig = {
name: string,
label?: Label
}
这样每个方法,我们都可以通过按键事件一次性地注册,按下,按起,持续按住的3个状态。
程序使用时候也比较简单
aa.ctrl
.add("forward",[KeyCode.KEY_W],this._moveForward,this.labels[0])
.add("backward",[KeyCode.KEY_S],this._moveBackward,this.labels[1])
.add("left",[KeyCode.KEY_A],this._moveLeft,this.labels[2])
.add("right",[KeyCode.KEY_D],this._moveRight,this.labels[3])
.add("dance",[KeyCode.KEY_X],this._danceAction,this.labels[4])
.add("rap",[KeyCode.KEY_N,KeyCode.KEY_B],this._rapAction,this.labels[5])
.add("basketball",[KeyCode.KEY_C,KeyCode.KEY_X],this._basketballAction,this.labels[6])
一次性就给角色添加了上下左右移动和唱跳rap篮球的操作。
同时我们使用了名字作为KeyBinding的键值,这样就可以在游戏内实时的去修改任意行为的按键绑定。
游戏内动态切换按键绑定时候,我们先通过行为事件名称做键值,然后动态修改eyBinding的KeyCombo按键组合即可。
这里的多按键判定比较简单,会依次记录150ms内按下的按键,来判定是否是组合按键。
获取本文源码
为了制作这个Demo,二喵亲自制作了iKun的手绘贴图模型,布线精良,适合于休闲游戏。
关注老菜喵,回复 “ctrl” 即可免费获取本文Demo。
点击阅读原文线上试玩