设为首页收藏本站喵玉殿官方微博

 找回密码
 少女注册中
搜索
查看: 7013|回复: 9

[编程算法] [11/19更新]Unity用输入处理系统 设计思路

[复制链接]
发表于 2014-11-18 22:49:37 | 显示全部楼层 |阅读模式
本帖最后由 larrymario 于 2014-11-19 22:00 编辑

游戏项目稍微有了点进展,于是就来讲讲目前完成的输入处理部分的设计思路。算是半个教程,半个开发心得吧。吐槽热烈欢迎。

适用人群:
* 游戏开发者(大触求指导
* 对游戏开发感兴趣但和我一样没啥经验的人(求共勉
* 知道Unity大概怎么用的人
* 觉得Unity很赞的人
* 觉得UnityChan很可爱的人

Unity自带非常好用的输入系统,足够应对一般游戏的上下左右跳跃射击等基本动作的需求,比如像这样:
if (Input.GetAxis("Jump") > 0.001f) {
        //跳跳跳
}

不过,如果输入较为复杂,比如一些ACT游戏中常见的“前前”冲刺指令,甚至是格斗游戏里多个方向键+攻击键的输入序列,光用Unity自带的输入系统可能就不够了,因为还要涉及到当前按键之前的按键信息。这也就意味着,必须有一个存储输入序列的变量。
既然直接用是不行了,那就索性搞一个泛用性强的系统吧?这样方便扩展可用的指令数量,也方便今后的开发任务能直接重用。
于是,就萌生了这样的一个设计思路:
1、先处理Unity自带输入系统的信息,转换成一个更方便处理的值。
2、对这个值进一步处理,根据处理结果改变主角(也就是魔理沙,虽然目前是UnityChan……)的“状态”。
3、根据这个状态,让主角进行各种各样的动作。

似乎有点意思,那就进一步设计:
变量:
按键值keyCode,按键时间keyTime
(按键时间也是必须要记录的信息,短按键和长按键意义不同这个应该可以想象吧)
按键值序列keyCodeQueue,按键时间序列keyHoldTimeQueue
流程:
1、处理Unity原生输入信息,转换为按键值keyCode。
2、如果现在这个keyCode与上一个keyCode相同,那就使按键时间keyTime加一;否则,将前一个keyCode放入按键值序列
keyCodeQueue,将前一个keyTime放入按键值序列keyHoldTimeQueue,同时归零重新计算。
3、结合队列中的按键信息,对这个keyCode进行处理,决定角色现在应该什么状态。(再以冲刺为例,符合条件的输入是当前按下了“前”,而队列中的第一个与第二个按键信息分别为“短暂的啥都不按”和“短暂的前按键”。简单的输入则只需看当前按下了什么键即可)
4、根据这个状态让主角做动作。

差不多有点样子了。那么接下来一个问题就是:keyCode应该怎么设计?

整个项目都在这里了,包括没说完的部分:https://github.com/larrymario/MarisaStrike
欢迎关注,Pull Request大欢迎。
[2014/11/19更新]
下面来谈谈keyCode的设计。
因为有很多按键,所以容易想到,肯定不能一个变量管理一个按键信息,那样满编辑器都是变量了,程序就很不优雅。可行的方法之一就是设计一个类,使其管理所有按键信息,并提供接口供主要流程部分调用。那么我们可以这样设计:
public class keyCode {
        bool up;
        bool down;
        bool left;
        bool right;
        //......

        bool pressKey(int keyCodePos);        //使某个按键处于按下状态
        bool releaseKey(int keyCodePos);//使某个按键处于弹起状态
        //以及各种获取信息的函数
}
具体实现就略去了。如此,在主要控制流程部分只需实例化一个keyCode,就可以对所有按键信息进行管理。

不过呢……本人并没有这么做,而是采用了另外一种更高(zhuang)效(bi)的设计:位图。
所谓位图,既是指图片的一种保存形式,也是指一种数据结构。

我们来从另一个角度来考虑常用的数据类型,int。在C#中,int或uint是32位的(大概,编译器不同会有差别),也就是32个二进制位。每一位,诸位都知道,可以存储一个二元的信息(即有或没有)。
回过头来再来考察我们的keyCode,其实她需要管理的正是一群二元信息——按钮按下去或者不按下去。那么,其实可以直接用int来作为keyCode的数据类型,并用与往常不同的方法来看待这个int:
Up        Down        Left        Right        Re1        Re2        Fire        Jump
0        0        0        0        0        0        0        0
因为不需要32位那么多,所以现在就取低八位来使用。可以看到,我把每一位都对应到一个按键上去,于是这一个二进制位就可以反映这个按键是否被按下。从效果上说,和我设计一个有很多bool的类的效果是一样的。

那么怎么使用呢?这里就可以用到C#(以及大多数泛用性高的语言都有的)位运算。
举例来说,我现在按下了Jump,那我就可以对keyCode进行这样的运算:
if (Input.GetAxis("Jump") > 0.001f) {
        keyCodeTemp |= 1;
}
这里对keyCodeTemp(keyCode的临时变量)进行了逻辑或运算,这时候原来的keyCode 0000 0000就变成了0000 0001,代表我按下了跳跃键。对于高位的键,可以将对应的二进制值转为十进制值,比如说如果是Jump(0000 0010),那就是keyCodeTemp |= 2,Right就是16,以此类推。多个键同时按下的话,那就是有几个位是1。举例来说,0010 0011表示我同时按下了Left、Fire、Jump。
那么要分析按键的时候怎么办?
可以这样:
if ((keyCode & 2) > 0) {
        //射射射
}
这里对keyCode进行逻辑与运算,由此可以得知Fire位有没有被按下。用这种方法也可以避免其他位对这个判断的干扰。
大致上的使用方法就是这样。为了让代码更加直观,而不是看着一堆数字,我们可以创建一个枚举类,如下:
enum KeyMap
{
        Up = 128,
        Down = 64,
        Forward = 32,
        Backward = 16,
        Reserve1 = 8,
        Reserve2 = 4,
        Fire = 2,
        Jump = 1,
        Idle = 0
};
那么在实际使用的时候就可以变成这样:
if (Input.GetAxis("Fire1") > 0.001f) {
        keyCodeTemp |= (uint)KeyMap.Fire;
}

if ((keyCode & (uint)KeyMap.Fire) > 0) {
        charState = State.Shoot;
}
相对来说要直观一点了。

正是因为可以直接使用位运算,使得这个方法可以比设计一个类更高效(大概吧……)。当然缺点是有的,就是不管是自己还是别人理解起来都有点麻烦(而且现在的CPU这点运算量还承受不起么)。
嘛,毕竟这是每一帧都要做的事情,能省就省吧。

keyCode的设计就谈到这里。下面正好再讲下按键队列的工作机制。
基本流程之间已经说了,这里就举个例子,看下按键队列中的情况。
还是以冲刺指令“前前”为例:
按键前(假设我还啥都没按过,这是初始化后的队列)
keyCodeQueue                0 0 0 0 0 0 0 0
keyHoldTimeQueue        8 8 8 8 8 8 8 8
keyCode = 0
按键时(左键按下,前面那个123当做是没按键的时间,也会被加入队列)
keyCodeQueue                0 0 0 0 0 0 0 0
keyHoldTimeQueue        8 8 8 8 8 8 8 123
keyCode = 32
此时识别系统看到keyCode = 32,会往前搜一个短时间的0,发现没有,只有一个长0,于是识别不成功,仅对“向前”作出相应。
松开,再放开,再按下,此时:(时间就随便算一个短的,总之小于8帧)
keyCodeQueue                0 0 0 0 0 0      32 0
keyHoldTimeQueue        8 8 8 8 8 123  4   6
keyCode = 32
这时识别系统又看到了keyCode = 32,往前搜一个短0,发现有,再找一个短的32(前按键,不是“人”的32,请不要误解ww),发现也有,一个完整的前冲指令就被识别出来了,于是主角开始前冲。
大致上就是这样。截个图:
12.png
可以看到左侧的调试信息。
图中跑步动画已经做了,不过还没做完也还没上传。嗯,下一次就讲下Unity动画系统的(不正确)使用方法吧。

那么这次就先说到这里。下面一部分是状态处理,前面的代码里有一句话已经涉及到了。不过因为还没完全做完,所以还是留到下一次……再下一次开个新帖吧。(其实这才是重点不是么

发表于 2014-11-18 23:26:14 | 显示全部楼层
嗯。。。还有13分钟断电。。。
我看看能写多少。。。

KeyCode这东西。。。其实可以通过这个API获取,缺点是会被杀软误报,而且是全系统监视键盘。。。或者通过KeyEventArgs也可以,但是涉及到KeyDown,KeyPress,KeyUp三个事件。。。我现在用的是API
  1. Public Declare Function GetKeyState Lib "user32.dll" (ByVal KeyCode As Integer) As Short
复制代码
这个,嗯,下面以API为准
这里有个函数,因为API可以检测当前按下还是过去按过这个键,这函数是检测当前按键的。。。
  1. Public Function GetKey(ByVal KeyCode As Integer) As Boolean
  2.         If &H8000 = (&H8000 And GetKeyState(KeyCode)) Then
  3.             Return True
  4.         Else
  5.             Return False
  6.         End If
  7.     End Function
复制代码
KeyCode是常量,可以查到。。。虽然C++是通过一个布尔型数组来处理按键。。。至少VB,C#是常量
  1. Public Enum Keys As Integer
  2.      System.Windows.Forms 的成员
  3. 摘要:
  4. 指定键代码和修饰符。
复制代码
对于多按键响应,应该在不同的If内进行检测,比如:(STG里的)
  1. If GetKey(Keys.ShiftKey) Then
  2.             If GetKey(Keys.Up) Then
  3.                 Player1.y = Player1.y - 3
  4.             End If
  5.             If GetKey(Keys.Down) Then
  6.                 Player1.y = Player1.y + 3
  7.             End If
  8.             If GetKey(Keys.Left) Then
  9.                 Player1.x = Player1.x - 3
  10.             End If
  11.             If GetKey(Keys.Right) Then
  12.                 Player1.x = Player1.x + 3
  13.             End If
  14.         Else
  15.             If GetKey(Keys.Up) Then
  16.                 Player1.y = Player1.y - 9
  17.             End If
  18.             If GetKey(Keys.Down) Then
  19.                 Player1.y = Player1.y + 9
  20.             End If
  21.             If GetKey(Keys.Left) Then
  22.                 Player1.x = Player1.x - 9
  23.             End If
  24.             If GetKey(Keys.Right) Then
  25.                 Player1.x = Player1.x + 9
  26.             End If
  27.         End If
复制代码
对于双击按键的话,你或许还需要
  1. Public Declare Function GetTickCount Lib "kernel32" Alias "GetTickCount" () As Long
复制代码
这个计时API,通过检测两次按键的时间间隔来判断按键。。。

先发了,后面再改。
回复

使用道具 举报

发表于 2014-11-19 00:34:30 | 显示全部楼层
其实是个按键的“连击系统”蹦蹦蹦~~

点评

不光是连击啦,你可以用这个输入系统玩格斗游戏了(当然现在只有输入系统……  发表于 2014-11-19 07:33
回复

使用道具 举报

 楼主| 发表于 2014-11-19 07:30:53 | 显示全部楼层
本帖最后由 larrymario 于 2014-11-19 08:03 编辑
drzzm32 发表于 2014-11-18 23:26
嗯。。。还有13分钟断电。。。
我看看能写多少。。。

嘛……因为有Unity自带的输入系统了,所以我也不用直接去抠键盘的输入信号了,而且Unity对手柄的支持和键盘鼠标一视同仁,好方便好方便的。讲这个的目的其实就是分享一下思路,“怎么取得按键信号”只是第一步,关键是后面的:
1、我对keyCode的一种设计模式;
2、读完keyCode后对其进行的一种相对比较通用的分析方法(大坑,说实话我只有基本的思路,具体实现很谜,详见Assets/Standard Assets/Scripts/MarisaController.cs);
3、分析完后转换成了主角的State,怎么处理State。
嗯……就是不知道这样实际效率怎么样……嘛反正现在cpu都很厉害
回复

使用道具 举报

发表于 2014-11-19 07:40:47 | 显示全部楼层
本帖最后由 krh 于 2014-11-18 18:42 编辑

按lz的方法来的话,这个队列恐怕每次update()都要被扫描一遍吧,比较浪费

……可能也不是很浪费,毕竟按键队列不可能很长
在下认为这篇博客中的内容比较合适
http://blog.csdn.net/qinyuanpei/article/details/38023199

也就是用状态表示:
比如
下 -》状态1 -》下 -》状态2 -》拳 -》发招1
下 -》状态1 -》下 -》状态2 -》踢 -》发招2
下 -》状态1 -》前 -》状态3 -》拳 -》发招3

就像是绯想天则的出招表了
这样一个update就只监听一个按键事件了

点评

唔……写在fixedUpdate里会出什么事么?我现在就是全部写在里面的,貌似还没出事的样子……  发表于 2014-11-19 08:32
krh
那样的话,确实lz的方案看不出什么问题呢,唯一要注意的就是别忘了——玩家按P暂停时,要不要清除队列,另外别把队列维护写在fixedupdate()里  发表于 2014-11-19 08:22
用状态机的方式我也想过,但是那样需要的中间态太多了,不好办……  发表于 2014-11-19 08:02
不用的,只在按下了可能有后续动作的按键之后才会被扫描一次,而且只扫描规定的数量,一旦发现不匹配就不继续扫了。啊,不过分享教程感谢~  发表于 2014-11-19 07:49
回复

使用道具 举报

发表于 2014-11-19 09:23:16 | 显示全部楼层
因为暂停功能一般来说是用 timescale =0 来实现的,
timescale 一般是1,也就是正常速度,timescale =0.5 的话,游戏引擎的运行速度减半
也就是说,游戏世界中时间减速了(用这个可以非常简单地实现子弹时间)

当timescale =0 是,时间就停止了,暂停也就实现了
但是这会影响到 fixedupdate() 因为 fixedupdate() 和引擎的运行时间有关
timescale =0 的时候,fixedupdate() 是不会被调用的
http://docs.unity3d.com/ScriptReference/Time-timeScale.html

但是update()还会被调用,onGUI()也会被调用,因此可以在暂停期间监听用户操作(比如退出游戏)

对lz来说,如果暂停之前把暂停键的keycode 也放进队列里,可能会引起bug(因为暂停期间 fixedupdate() 不会被调用,也就无法维护队列),所以才有此建议


点评

另外关于暂停这个事情,其实我本来就是打算用timeScale来暂停的,所以就全部写到fixedUpdate里去了。其实我就是在研究怎么暂停的时候,才知道fixedUpdate的这个特性,之前以为只是个能自定义时间特性的update罢了www  发表于 2014-11-19 12:32
我明白意思了……目前我的想法是,暂停键不算是“角色控制”的一部分,所以由另外的脚本响应,不包括在这个角色控制的脚本里,这样的话就不会干扰到按键队列的维护问题了。  发表于 2014-11-19 12:24
回复

使用道具 举报

发表于 2014-11-19 09:48:04 来自手机 | 显示全部楼层
本帖最后由 drzzm32 于 2014-11-19 10:48 编辑

嘛手机上写点什么……

核心看起来是……怎么说来着?按键队列处理?
来看看这函数:(我用类C写个看看)
bool TestInput(int key1, int key2, int key3, int key4)

    //从key1按顺序检测,这玩意可以重载出其他数目的参数
    //以下函数bool GetKey(int)是获取当前按键状态的
    //Sleep(int)为延时函数,参数单位为毫秒
    if(GetKey(key1))
    {
        Sleep(100);
        if(GetKey(key2))
        {
            Sleep(100);
            if(GetKey(key3))
            {
                Sleep(100);
                if(GetKey(key4))
                {
                    Sleep(100);
                    return true;
                };
            };
        };
    };
};
这个只是顺序按键而且我刚才脑洞出来的…………
其实最好做成非顺序按键的感觉要好点?

点评

我又想到一个方法………详细看回复  发表于 2014-11-19 14:41
其实这样,你把“不按键”也想象成一种按键,那样就判断一组连续的按键是不是符合要求,就行了。说到底肯定是要有个队列来存储按键信息的。  发表于 2014-11-19 12:29
32这样不行的啦……如果Sleep的话就把整个程序阻塞住了(先不管Unity我就从Sleep原来的特性来看),什么事情都不能做了。  发表于 2014-11-19 12:27
回复

使用道具 举报

发表于 2014-11-19 13:38:11 | 显示全部楼层
本帖最后由 lrdcq 于 2014-11-19 18:47 编辑

来详细的描述一下我这里的方法,就是我3楼所说的“连击系统”,事实上整个控制部分分为两部分
1.按键链  2.连击系统
这两大系统把按键控制计时控制分开区别,它的上级基于onkeydown,onkeyup这两个事件,下级就是实际的技能出招了

1.按键链。
事实上本来想说它是个链表,不过高级语言很多没有实际的链表结构,所以就说是个链就行了。
这个链不是栈也不是队列,具体操作是:
onkeydown事件发生,按下的按键keycode作为一个元素从一端加入链
onkeyup事件发生,从链中找到并删除keycode=当前keycode的元素
也就是说,这链维护了当前按下的按键值与顺序
好,够了


2.连击系统
也就是上面讨论中大家纠结的如何time如何sleep这个事情,如果把它单纯理解为一个连击的话就比较清晰了
例如,我希望FTG中搓招每个按键的间隔时间少于300ms,也就是说我的键盘输入要维持300ms间隔以内的连击那么,我需要一个300ms的计时器
每次触发onkeydown,就是按下一个技能按键时,中断这个计时器(如果有)并重启这个计时器
显然,如果其中一个计时器是被动中断的,好的,300ms以内按下的按钮,没事没事
如果是300ms时间到了自己主动中断->OK,连击中断->回调函数里,我们可以出招了
这时回调函数做的事情就是读取按键链看看现在按顺序按下了哪些按钮,然后对应出招,就OK

END
这样的设计只能说“够用”,但是还不完善,请见谅


----------更新-----------

确实需要解决“只能存储“我同时按住了多少个键”数目的keyCode”这种问题


于是有了增加设计
我们还要维护另一个链,暂时就叫历史按键链吧,这次这个链是一个堆栈或者队列了
这个链中我们立刻压入刚刚从第一个按键链从弹出的数据
相应的,这个链的清空条件是:在连击触发结束后清空

这样,我们在连击结束的时候就可以查询到这次连击(上次连击的数据已经被清空了)过程中,按顺序曾经按下过的按键

点评

超烦,还有就是同时按键的判定问题,因为实际按的时候通常都不是真的“一起按”的,而是先按了某个再按同时按住,于是这个又要针对不同情况写一堆判定,总之就是程序的优雅度各种下降(叹  发表于 2014-11-19 22:27
确实,最烦的是搓技能过程中会出现“快速按过几个按键”和“快速按上几个按键”,得区别对待也是醉了  发表于 2014-11-19 22:07
哦,这样我就明白了。嘛……我觉得用定时器控制好麻烦的所以就没这么做,而是把不按键也当成一种按键来处理,不过说到底我现在这个队列,也是建立在这个设计的基础上的  发表于 2014-11-19 21:47
@larrymario 确实有问题。。。更新了一下设计。。。  发表于 2014-11-19 18:47
——我也想过用这样的方法实现,而且确实能节省空间,不过……感觉写起来好烦啊(躺),关键一点就是对于“不按键”这个事件,我觉得处理起来很麻烦……  发表于 2014-11-19 16:29
有一点我不明白……onKeyUp的时候就要删除这个keyCode?那样岂不是这个链里只能存储“我同时按住了多少个键”数目的keyCode么……连击系统我似乎是明白了,就是靠计时器来判定是否还在一个连续的按键序列里,——  发表于 2014-11-19 16:27
回复

使用道具 举报

发表于 2014-11-19 14:45:41 | 显示全部楼层
IMG_20141119_144002.jpg
发个图……慢慢看吧……
数字需要仔细选择……
应该能理解我的想法吧……
需要进行复位……看起来……

点评

懂了,其实32你这张纸上写的和我现在做的是差不多的ww不过对于按键值的处理我有更好的方法(大概),过会儿更新的时候再说  发表于 2014-11-19 16:31
回复

使用道具 举报

发表于 2014-12-11 17:00:53 | 显示全部楼层
本帖最后由 Darksword 于 2014-12-13 12:23 编辑

哦,unity没用过,c++和dx党倒是对这个问题处理的很自由,这里也是主要攻act向的~
本来我也是这样靠if和一堆判断堆起来的,但是目前我的通用状态机架构(仿Mugen)也有雏形了,以后应该会全面依赖状态机来处理这些显然是状态切换相关的操作,毕竟这里c++党可不能依赖硬编码嘛


@LarryMario (话说。。我生日比你大6天=w=)
虽说是仿mugen,但架构是我自己写的,因为我只是读了它的手册和例子,看不到源代码。
我想过两种,一个是层次关系型state布局,父子或者下属节点这种,一个是并列state,靠属性区分类别,但是最好有个根状态来控制状态
关系型实际中不够灵活,目前我用的属性,例如ActionType="Attack"为攻击状态,idle为空闲状态。(Mugen也是这样)
不同的属性来确定某一状态描述的维度。(一个状态包含一个n维向量)

但是一个状态还有相应的逻辑,这部分我是自己写的简单脚本,利用xml,如图:
QQ截图20141213115456.jpg

(不要吐槽数据表什么的。。我刚刚学完数据库,还会改架构。)
逻辑由controller控制,每个controller分为trigger(触发器,我理解为封装的if)和执行器executor(这些实现都需要在c++里自定义,但是因为是模块化所以还比较方便)

大意我想都是英文,都能看得懂。

关于状态切换,在根状态添加,如图:
QQ截图20141213120029.jpg
也是很好理解,不细说了。
(主要我现在只是雏形,欢迎各位大大吐槽)

我这种状态机最终目的是通用,只要最终在自己定义的类注册属性和方法就行了。。

如果是多个state,我觉得也很好,从数据库的角度看避免的数据冗余,但是增加了判断次数,实际效率不知道影响多大。
游戏编程设计总是有大量的效率与内存占用的斗争。。。
不过话说你都是怎么用多个state的。。如果是正交的状态也是无妨。。


点评

求讲解mugen里的状态机架构是怎样的,说实话我现在这个在实际做的时候感觉有问题了……目前项目做到现在,已经从一个state变成了多个state共同控制角色状态,光是一个state我已经不知道该怎么管理状态了(捂脸  发表于 2014-12-12 19:23
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 少女注册中

本版积分规则

合作与事务联系|无图版|手机版|小黑屋|喵玉殿

GMT+8, 2025-10-31 11:21

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表