自从开始做智能手机游戏以后就没有再处理过按键的相关内容了。最近游戏接触了steam平台,需要支持键盘鼠标,手柄摇杆,所以又开始弄这些事情。对于按键和摇杆,需要处理的内容大致可分为一下几点:
输入转换器
输入转换器的作用是根据键值对照表,将各平台传入的键值转换为自定义键值。
- 自定义键值
游戏中只会对应一类键值,也就是我们自定义的键值,自定义键值的特点是以二进制的一位表示一个键值,这样可以很容易的识别多个按键同时按下的操作,而且易于扩展,不论是哪个平台的各种按键,只要填好键值对照表即可,游戏中无需做任何修改。
/**
* 自定义键值
*/
export enum InputKeyCode {
ENTER = 1, // A
D = (1 << 1), // Y
S = (1 << 2), // X
Back = (1 << 3), // B
Ctrl = (1 << 4), // LB
Shift = (1 << 5), // RB
ArrowL = (1 << 6), // LT
ArrowR = (1 << 7), // RT
Esc = (1 << 8), // Menu
Up = (1 << 9), // 上
Left = (1 << 10), // 左
Down = (1 << 11), // 下
Right = (1 << 12), // 右
M = (1 << 13),
N = (1 << 14),
B = (1 << 15),
PERIOD = (1 << 16),
COMMA = (1 << 17),
}
- 按键与键值的映射
可以理解为键盘上的按键名称与键值的对应关系。主要用于解决多平台键值不同的情况,所以在不同的平台按键名称可能是不同的。每个平台对应有一个这样的映射数据。
/**
* 按键对应的自定义键值
*/
export const SELF_KEY_CODE = {
ENTER: InputKeyCode.ENTER,
ESCAPE: InputKeyCode.Esc,
PERIOD: InputKeyCode.PERIOD,
COMMA: InputKeyCode.COMMA,
D: InputKeyCode.D,
S: InputKeyCode.S,
BACKSPACE: InputKeyCode.Back,
CTRL_LEFT: InputKeyCode.Ctrl,
SHIFT_LEFT: InputKeyCode.Shift,
M: InputKeyCode.M,
N: InputKeyCode.N,
B: InputKeyCode.B,
Up: InputKeyCode.Up,
Left: InputKeyCode.Left,
Down: InputKeyCode.Down,
Right: InputKeyCode.Right,
RT: InputKeyCode.ArrowR,
LT: InputKeyCode.ArrowL
}
- 各平台相同操作对应的按键名称
主要用于表示在相同的自定义按键名称下,不同平台对应的按键名称有哪些。因为是列表形式,所以支持不同按键拥有相同效果的情况,比如按下X和Y都是攻击。次配置数据可以用配置表方式让策划填写。
/**
* 各个平台操作方式的映射表
*/
export let KeyCodeMatch: KeyMatchInfo[] = [
//在cocos中按下ENTER建 和 在steam手柄中按下A建的效果都是自定义按键ENTER触发的功能
{ self: "ENTER", cc: ["ENTER"], steam: ["A"] },
{ self: "ESCAPE", cc: ["ESCAPE"], steam: ["Menu"] },
{ self: "PERIOD", cc: ["PERIOD"], steam: [] },
{ self: "COMMA", cc: ["COMMA"], steam: [] },
{ self: "D", cc: ["D"], steam: ["Y"] },
{ self: "S", cc: ["S"], steam: ["X"] },
{ self: "BACKSPACE", cc: ["BACKSPACE"], steam: ["B"] },
{ self: "CTRL_LEFT", cc: ["CTRL_LEFT"], steam: ["LB"] },
{ self: "SHIFT_LEFT", cc: ["SHIFT_LEFT"], steam: ["RB"] },
{ self: "M", cc: ["M"], steam: [] },
{ self: "N", cc: ["N"], steam: [] },
{ self: "B", cc: ["B"], steam: [] },
{ self: "Up", cc: ["Up"], steam: ["Up"] },
{ self: "Left", cc: ["Left"], steam: ["Left"] },
{ self: "Down", cc: ["Down"], steam: ["Down"] },
{ self: "Right", cc: ["Right"], steam: ["Right"] },
{ self: "LT", cc: [], steam: ["LT"] },
{ self: "RT", cc: [], steam: ["RT"] },
]
- 转换器的实现InputConverter
import { KeyMatchInfo, SELF_KEY_CODE } from "./InputConfig";
/**
* 键值转换器
*/
export default class InputConverter {
private static ins: InputConverter
static getInstance() {
if (!this.ins) {
this.ins = new InputConverter();
}
return this.ins;
}
/**
* key 平台名称
* key 平台键值
* value 自定义的键值
*/
private keyMap: { [key: string]: { [key: number]: string } } = {}
private selfValueKey: { [key: number]: string } = {}
protected config: KeyMatchInfo[] = null;
protected keyCodeMap: { [key: string]: any } = {}
getKeyName(keyCode: number) {
return this.selfValueKey[keyCode]
}
/**
* 添加一个平台的键值表
* @param platform 平台名称 如 cc steam
* @param KEY_CODE 按键与键值的对应数据
*/
addKeyCode(platform: string, KEY_CODE: any) {
this.keyCodeMap[platform] = KEY_CODE;
this.setKeyCodeConfig(platform)
}
init(config: KeyMatchInfo[]) {
this.config = config;
for (const platform in this.keyCodeMap) {
this.setKeyCodeConfig(platform)
}
}
private setKeyCodeConfig(platform: string) {
let KEY_CODE: any = this.keyCodeMap[platform]
if (this.config && !this.keyMap[platform] && KEY_CODE) {
this.keyMap[platform] = {}
for (const index in this.config) {
const element = this.config[index];
this.selfValueKey[this.getKeyValueByKeyName(element.self)] = element.self;
let list = element[platform];
for (let k = 0; k < list.length; k++) {
const key = KEY_CODE[list[k]];
// console.log("InputConverter platform ", platform, key, element.self)
this.keyMap[platform][key] = element.self;
}
}
}
}
/**
* 键值映射,通过平台的key,或者使用的key
* @param name 平台名称
* @param key
* @returns
*/
getKeyValue(name: string, key: number) {
let data = this.keyMap[name]
if (data) {
let id = data[key]
return this.getKeyValueByKeyName(id)
} else {
console.warn(' data is null ')
}
return 0;
}
getKeyValueByKeyName(name: string) {
return SELF_KEY_CODE[name];
}
}
输入管理器
输入管理器负责两件事情,一是将键值分发给监听者,二是记录键值,供外界使用,
- 记录键值
记录的键值也就是记录多按键同时操作的结果。
export default class GameInputEvent {
private _keyCode: number = 0;
/**
* 设置按键键值
*/
public set keyCode(keyCode: number) {
this._keyCode = keyCode;
}
/**
* 获取按键键值
*/
public get keyCode() {
return this._keyCode;
}
/**
* 键值是否包含指定按键
*/
public isContainKeys(keys: number): boolean {
return (this._keyCode & keys) == keys;
}
}
- 监听者
所有需要实现按键操作的地方都需要实现这个接口并注册到输入管理器中。这是一步非常庞大的劳动,是不是我们每个界面都需要实现这个接口呢?这个我会另开一片文章单独讲解。
export default interface GameInputListener {
/**
* 纯键盘按键按下回调
* @param keyCode 按键键值
*/
onKeyPress(keyCode: number): void;
/**
* 纯键盘按键抬起回调
* @param keyCode 按键键值
*/
onKeyRelease(keyCode: number): void;
/**
* 是否阻挡事件穿透
*/
isBlockInput(): boolean;
/**
* 摇杆事件
* @param x -1 到 1
* @param y -1 到 1
* 每个方向都是-1到1之间的实数,-1是左/下,1是右/上
*/
onRockerEvent(index:number,x: number, y: number): void
}
- 输入管理器GameInputMgr
import GameInputEvent from "./GameInputEvent";
import GameInputListener from "./GameInputListener";
/**
* 游戏键鼠,手柄转换器
*/
export default class GameInputMgr {
private static ins: GameInputMgr
static getInstance() {
if (!this.ins) {
this.ins = new GameInputMgr();
}
return this.ins;
}
protected inputEvent: GameInputEvent = new GameInputEvent();
protected listeners: GameInputListener[] = []
on(listener: GameInputListener) {
this.listeners.push(listener)
}
off(listener: GameInputListener) {
let index = this.listeners.indexOf(listener)
if (index >= 0) {
this.listeners.splice(index, 1)
}
}
onKeyPress(key: number) {
if (key == undefined) {
return;
}
if (this.inputEvent.isContainKeys(key)) {
return;
}
this.inputEvent.keyCode |= key;
for (let index = 0; index < this.listeners.length; index++) {
const element = this.listeners[index];
element.onKeyPress(key)
}
}
onKeyRelease(key: number) {
if (key == undefined) {
return;
}
if (!this.inputEvent.isContainKeys(key)) {
return;
}
this.inputEvent.keyCode &= ~key;
for (let index = 0; index < this.listeners.length; index++) {
const element = this.listeners[index];
element.onKeyRelease(key)
}
}
getKeyEvent() {
return this.inputEvent;
}
onRockerEvent(keyIndex: number, x: number, y: number): void {
for (let index = 0; index < this.listeners.length; index++) {
const element = this.listeners[index];
element.onRockerEvent(keyIndex, x, y)
}
}
}
不同平台的输入处理
这部分也非常独立,一个平台对应一个类,主要处理针对平台的操作,目的是拿到平台按键对应的键值,这样再通过键值转换器就可以得到自定义键值了。键值管理器只会接收自定义键值。
- CocosCreator
import { EventKeyboard, KeyCode } from "cc";
import { Input } from "cc";
import { EventMouse } from "cc";
import { input } from "cc";
import GameInputMgr from "../input/GameInputMgr";
import { InputType } from "../input/InputConfig";
import InputConverter from "../input/InputConverter";
let CC_KEY_CODE = {
ENTER: KeyCode.ENTER,
ESCAPE: KeyCode.ESCAPE,
PERIOD: KeyCode.PERIOD,
COMMA: KeyCode.COMMA,
D: KeyCode.KEY_D,
S: KeyCode.KEY_S,
BACKSPACE: KeyCode.BACKSPACE,
CTRL_LEFT: KeyCode.CTRL_LEFT,
SHIFT_LEFT: KeyCode.SHIFT_LEFT,
M: KeyCode.KEY_M,
N: KeyCode.KEY_N,
B: KeyCode.KEY_B,
Up: KeyCode.ARROW_UP,
Left: KeyCode.ARROW_LEFT,
Down: KeyCode.ARROW_DOWN,
Right: KeyCode.ARROW_RIGHT
}
export default class CocosInputMgr {
private static ins: CocosInputMgr
static getInstance() {
if (!this.ins) {
this.ins = new CocosInputMgr();
}
return this.ins;
}
protected platName: string = InputType.CC;
start() {
InputConverter.getInstance().addKeyCode(InputType.CC, CC_KEY_CODE)
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
input.on(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
input.on(Input.EventType.MOUSE_UP, this.onMouseUp, this);
input.on(Input.EventType.MOUSE_MOVE, this.onMouseMove, this);
}
onDestroy() {
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
input.off(Input.EventType.MOUSE_DOWN, this.onMouseDown, this);
input.off(Input.EventType.MOUSE_UP, this.onMouseUp, this);
input.off(Input.EventType.MOUSE_MOVE, this.onMouseMove, this);
}
protected onKeyDown(event: EventKeyboard) {
let keyValue = InputConverter.getInstance().getKeyValue(this.platName, event.keyCode)
GameInputMgr.getInstance().onKeyPress(keyValue)
}
protected onKeyUp(event: EventKeyboard) {
let keyValue = InputConverter.getInstance().getKeyValue(this.platName, event.keyCode)
GameInputMgr.getInstance().onKeyRelease(keyValue)
}
protected onMouseDown(event: EventMouse) {
}
protected onMouseUp(event: EventMouse) {
}
protected onMouseMove(event: EventMouse) {
}
}
- steam
import { v2, Vec2 } from 'cc';
import GameInputMgr from '../input/GameInputMgr';
import { InputType } from '../input/InputConfig';
import InputConverter from '../input/InputConverter';
export const ACTION_SET = ["GameControl"];
export const DIGITALS = ["A", "Y", "X", "B", "LB", "RB", "LT", "RT", "Menu", "Up", "Left", "Right", "Down"];
export const ANALOGS = ["Move", "Look"];
//steam键值 根据DIGITALS 的顺序得来
export enum SteamKeyCode {
A, // A
Y, // Y
X, // X
B, // B
LB, // LB
RB, // RB
LT, // LT
RT, // RT
Menu, // Menu
Up, // 上
Left, // 左
Down, // 下
Right, // 右
}
const VEC_RIGHT = v2(1, 0);
const vectorBuff = v2();
let DIR_LIST: SteamKeyCode[] = [SteamKeyCode.Up, SteamKeyCode.Down, SteamKeyCode.Left, SteamKeyCode.Right]
//八个方向对应的的steam的键值
let KEY_LIST: SteamKeyCode[][] = [
[SteamKeyCode.Right, SteamKeyCode.Up],
[SteamKeyCode.Up],
[SteamKeyCode.Left, SteamKeyCode.Up],
[SteamKeyCode.Left],
[SteamKeyCode.Left, SteamKeyCode.Down],
[SteamKeyCode.Down],
[SteamKeyCode.Down, SteamKeyCode.Right],
[SteamKeyCode.Right],
]
let KEY_CODE = {
A: SteamKeyCode.A,
Y: SteamKeyCode.Y,
X: SteamKeyCode.X,
B: SteamKeyCode.B,
LB: SteamKeyCode.LB,
RB: SteamKeyCode.RB,
LT: SteamKeyCode.LT,
RT: SteamKeyCode.RT,
Menu: SteamKeyCode.Menu,
Up: SteamKeyCode.Up,
Left: SteamKeyCode.Left,
Down: SteamKeyCode.Down,
Right: SteamKeyCode.Right
}
//https://partner.steamgames.com/doc/api/isteaminput
export default class SteamInputMgr {
private static ins: SteamInputMgr
static getInstance() {
if (!this.ins) {
this.ins = new SteamInputMgr();
}
return this.ins;
}
protected platName: string = InputType.STEAM;
private _gameCtrl: steam.SteamManager = null;
//存储摇杆触发的键值
private joyStickPressState: boolean[] = []
//左右方向的摇杆是否将摇杆数值改为方向键值。
private changeDirAnalog: boolean[] = [false, false]
setChangeDirAnlog(index: number, flag: boolean) {
this.changeDirAnalog[index] = flag;
}
start() {
if (this.isActive()) {
if (!this._gameCtrl) {
this._gameCtrl = steam.SteamManager.getInstance();
this._gameCtrl!.init(ACTION_SET, DIGITALS, ANALOGS);
this._gameCtrl!.activeActionSet(ACTION_SET[0]);
console.warn(' steam init ok ')
InputConverter.getInstance().addKeyCode(InputType.STEAM, KEY_CODE)
}
} else {
console.warn(' steam is error init ')
}
}
isActive() {
return window['steam'] != undefined;
}
/**
* 更新按键的状态
* @param digital
* @param _joyStickPress
* @param _joyStickRelease
*/
private updateState(digital: number[], _joyStickPress: number[], _joyStickRelease: number[]) {
for (let i = 0; i < digital.length; i += 2) {
let keyCode = digital[i];
// let keyValue = this.getKeyValue(keyCode)
if (digital[i + 1]) {
if (!this.joyStickPressState[i]) {
this.joyStickPressState[i] = true;
_joyStickPress.push(keyCode)
}
} else {
if (this.joyStickPressState[i]) {
this.joyStickPressState[i] = false;
_joyStickRelease.push(keyCode)
}
}
}
}
update(deltaTime: number) {
if (this._gameCtrl) {
this._gameCtrl!.update();
let _joyStickPress: number[] = [];
let _joyStickRelease: number[] = [];
let digital = this._gameCtrl!.getDigitalChangeData();
let analog = this._gameCtrl!.getAnalogChangeData();
this.updateState(digital, _joyStickPress, _joyStickRelease)
for (let i = 0; i < analog.length; i += 3) {
let keyIndex = analog[i];
if (this.changeDirAnalog[i]) {
vectorBuff.set(analog[i + 1], analog[i + 2]);
//手指离开 已经使用摇杆
let tempAnalogPress: number[] = this.getDirKeyCode(vectorBuff)
this.updateState(tempAnalogPress, _joyStickPress, _joyStickRelease)
} else {
GameInputMgr.getInstance().onRockerEvent(keyIndex, analog[i + 1], analog[i + 2])
}
}
if (_joyStickPress.length > 0) {
for (let index = 0; index < _joyStickPress.length; index++) {
let keyValue = InputConverter.getInstance().getKeyValue(this.platName, _joyStickPress[index])
GameInputMgr.getInstance().onKeyPress(keyValue)
}
}
if (_joyStickRelease.length > 0) {
for (let index = 0; index < _joyStickRelease.length; index++) {
let keyValue = InputConverter.getInstance().getKeyValue(this.platName, _joyStickRelease[index])
GameInputMgr.getInstance().onKeyRelease(keyValue)
}
}
}
}
/**
* 键值是否包含指定按键
*/
public isContainKeys(_keyCode: number, keys: number): boolean {
return (_keyCode & keys) == keys;
}
/**
* 通过给定的向量,获得向量对应的方向键的键值
* @param vectorBuff
* @returns
*/
private getDirKeyCode(vectorBuff: Vec2): number[] {
let state: number[] = []
for (let index = 0; index < DIR_LIST.length; index++) {
const keyCode = DIR_LIST[index];
state[index * 2] = keyCode;
state[index * 2 + 1] = 0;
}
if (vectorBuff.length() > 0.5) {
let angle = vectorBuff.angle(VEC_RIGHT) * 180 / Math.PI;
if (vectorBuff.y < 0)
angle = 360 - angle;
// console.log('getDirKeyCode angle ', angle, vectorBuff.x, vectorBuff.y)
let section = 0;
if (angle < 22.5)
section = 7;
else
section = Math.floor((angle - 22.5) / 45);
console.log('getDirKeyCode section ', section)
let keyList = KEY_LIST[section]
if (keyList) {
for (let index = 0; index < keyList.length; index++) {
const keyCode = keyList[index];
for (let j = 0; j < state.length; j += 2) {
if (state[j] == keyCode) {
state[j + 1] = 1
break;
}
}
}
console.log('getDirKeyCode state ', state)
}
}
return state;
}
}
初始化
首先初始化键值转换器,然后依次初始化其他平台的管理器即可完成初始化工作了。
import { _decorator, Component, Node } from 'cc';
import CocosInputMgr from './ccinput/CocosInputMgr';
import { config, KeyCodeMatch } from './input/InputConfig';
import InputConverter from './input/InputConverter';
import SteamInputMgr from './steam/SteamInputMgr';
const { ccclass, property } = _decorator;
@ccclass('InputComponent')
export class InputComponent extends Component {
start() {
InputConverter.getInstance().init(KeyCodeMatch)
SteamInputMgr.getInstance().start()
SteamInputMgr.getInstance().setChangeDirAnlog(0, true)
CocosInputMgr.getInstance().start()
}
onDestroy() {
CocosInputMgr.getInstance().onDestroy()
}
update(deltaTime: number) {
SteamInputMgr.getInstance().update(deltaTime)
}
}
类图
结语
我相信很多人都会用类似的设计方案来解决游戏中的按键处理。对于扩展平台是非常友好的。比如cocos最近支持了Switch主机,对于开发者来说,又有一片新的天地可以探索了,想想都非常激动。
如果大家有什么好的想法和建议,欢迎加入我的个人公众号给我留言,一起学习进步。