美文网首页
Unity——#20 Double Trigger & Long

Unity——#20 Double Trigger & Long

作者: MisakiMel | 来源:发表于2019-07-23 10:44 被阅读0次

      这节我们打算真正实现黑魂里面的跳跃机制,我先讲述的黑魂是怎么跳的。在黑魂中,它把后跳、翻滚、前跳、跑步做在了同一个按键(PC是Space,PS手柄是○)里,当角色静止不动时,玩家键入空格,将会是后跳;当玩家步行时,玩家键入空格,将会是翻滚;当玩家在奔跑(按住空格是奔跑)时,松开空格并在短时间内再次键入空格,将会是跑跳接翻滚。细想一下这真是一件非常复杂的事情。虽说复杂,但毕竟是前辈造好的轮子,我们抱着学习的心态去模仿终究不会太难。
      为了实现这个机制,我们应该实现两个检测,一个是Double Trigger,另一个是Long Press。

    • Double Trigger 按两次触发,即在短时间内快速键入同一个按钮才触发相应的动作,比较知名的例子就是按一下方向键走动,按两下方向键跑动(如DNF)。
    • Long Press 长按触发,即按住某个按键超过判断的时间范围才会触发相应的动作,如蓄力攻击,当然也有蓄力奔跑的。
        两个检测都涉及到一个非常关键的判断条件,那就是时间。触发Double Trigger的条件是在松开某个按键后(第一次键入完毕)的某个时间范围内是否有再次键入同个按键,如果有就触发,没有就不触发。触发Long Press的条件是在按住某个按键后的某个时间范围内有没有松手,没有就触发,有就不触发。
        到这我们就应该想到实现这两个检测之前,我们需要做什么前置工作,那就是实现一个键入复位反馈的机制和一个计时器。

    键入复位反馈机制

      先来讨论一下何为键入复位反馈机制。玩家键入一个按钮的信号图如下:


      键入就要在玩家按下按键那一刻,在信号上升沿完成后,产生反馈;

    复位就是在玩家松开按键那一刻,在信号下降沿完成后,产生反馈。

    只有侦测到这两个反馈,我们才能准确地调用计时器开始计时。
      我们先来实现这个键入复位反馈。我们创建一个C# Script,命名为My Button,在里面实现我们的反馈机制,赶快搞起来。
      我们应该需要3个bool变量来当做反馈的信号,第一个是IsPressing,代表的是第一幅图,与玩家的键入信号呈完全正相关;第二个是OnPressed,代表的第二幅图;第三个是OnReleased,代表的是第三幅图。
    public class MyButton {
        public bool IsPressing = false;
        public bool OnPressed = false;
        public bool OnReleased= false;
    }
    

      我们先用文字描述一下这三个布尔值怎样实现图中的信号变换。由于我们会用到外部传进来的输入信号(Input.GetKey()诸如此类的),所以我们需要一个bool变量来记录外部进来的信号,记录为当前的信号(curState)。

    • IsPressing 这个最简单,只要跟curState一致就行了。
    • OnPressed 在信号的上升沿处说明此时按键的信号正在切换,由false变为true。所以需要现在的状态(true)与之前的状态(false)比较,如果不同且此时的状态为true,说明现在处于键入的瞬间,OnPressed设为true,否则设为false。
    • OnRealeased 在信号的下降沿处说明此时按键的信号正在切换,由true变为false。所以同样亦需要现在的状态与之前的状态比较,如果两者不一且此时状态为false,说明现在处于松手的瞬间,OnRealeased设为true,否则设为false。
        没错,我们需要一个记录当前信号的curState和记录上一次信号的lastState:
    public class MyButton {
        public bool IsPressing = false;
        public bool OnPressed = false;
        public bool OnReleased= false;
    
        private bool curState = false;
        private bool lastState = false;
    }
    

      我们需要一个定义一个函数来实现刚才所说的变换,命名为Tick,另外,由于它需要外部的输入信号,所以要一个bool型参数:

        public void Tick(bool input){
            curState = input;
            IsPressing = curState;
    
            OnPressed = false;
            OnReleased = false;
    
            if (curState != lastState) {
                if (curState == true) {
                    OnPressed = true;
                } else {
                    OnReleased = true;
                }
            }
            lastState = curState;
        }
    

      这里的代码与刚的文字描述大体相同,不再赘述,唯一需要说的就是,要在这个函数的最后更新lastState,因为随着这个函数结束,当前状态其实就已经是过去式了。
      现在这个键入复位反馈机制就基本完成了,去试试看能不能达到我的需求。我打算在JoystickInput.cs(手柄)里做相关测试,键盘就不再另说了,其实都是一样的。
      取手柄的任意一个按钮,这里就用□来做测试:先创建一个MyButton变量并初始化:

    //在JoystickInput.cs里
        public MyButton ButtonA = new MyButton ();
    

      然后在Update调用它的Tick函数:

        void Update () {
            ButtonA.Tick (Input.GetKey (keyButA));
            print (ButtonA.IsPressing);
        ...
        }
    

      来看看IsPressing能否正常工作:



      嗯是可以的,在我没有按□时,打印false,在我按住□时,打印true。接下来再试试另外两位:

        void Update () {
            ButtonA.Tick (Input.GetKey (keyButA));
            print (ButtonA.OnPressed);
        ...
        }
    

      嗯也是可以的,在我没有按□时吐false,在我按下□那一刻就吐true,且即使我不松手也还是吐false。

        void Update () {
            ButtonA.Tick (Input.GetKey (keyButA));
            print (ButtonA.OnReleased);
        ...
        }
    

      最后一位也是不负众望。在我没按□时吐false,在我按住□时还是吐false,在我松开□那一刻吐true。这么说可能没有说服力,但是只能如此,我没有办法把我按手柄的情况拍下来上传到这里(捂脸)。
      现在键入复位反馈机制测试完毕,算是基本完工,不过以后肯定还会作出修改,因为还要联动计时器。现在的重点就来到了实现计时器上。

    计时器

      在Unity里做计时器一般都会用到Unity.EngineTime.deltaTime,因为它准确的记录了游戏进行时每一帧的时间间隔,我们要做的就是把它累积起来,那么就需要一个float变量来记录累积的时间,这就是计时。除了计时,我们还要一个float变量记录检测的时间范围,正因为要有时间上要求,才会有计时,不然计时毫无意义。我们还需要3个状态值代表目前的计时情况,我取它们为IDLE、RUN、FINSHED。

    • IDLE 计时器在开始计时前为闲置状态
    • RUN 计时器开始计时后进入该状态,表明当前计时器正在计时且没超出检测的时间范围
    • FINSHED 计时器因超出预定的时间范围停止计时并进入该状态
        OK,让我们看看代码如何实现,先创建一个C# Script,命名为My Timer,
    public class MyTimer{
    
        public enum STATE{
            IDLE,
            RUN,
            FINSHED
        }
    
        private float duration;    //检测时间范围
        private float elapsedTime;    //记录累积的时间
        public STATE state;    //记录当前状态
    }
    

      接下来这个计时器也需要一个Tick()函数来真正实现计时功能,我们使用switch判断state在哪个状态,并进行相应操作。需要注意的是,如果我们要在default层报错,是不能用print输出信息的,因为我们这个类没有继承MonoBehaviour,不过可以用Unity.Engine里面的Debug.log()来输出信息。

        public void Tick(){
            switch (state) {
            case STATE.IDLE:
                break;
            case STATE.RUN:
                elapsedTime += Time.deltaTime;
                if (elapsedTime > duration) {
                    state = STATE.FINSHED;
                    break;
                } else {
                    break;
                }
            case STATE.FINSHED:
                break;
            default:
                Debug.log("STATE Error!");      
                break;
            }
        }
    

      但这仅仅是计时而已,我们还需要一个开启计时的开关(把计时器状态设置为RUN),那么这个开关该放在哪里呢?我细想了一下,我把MyTimer里面的duration变量封装了,那么外界是不能访问它的(我也不想别人能轻易改变这个值),但是真正要开启计时器的是MyButton,因为只有MyButton才知道什么时候应该开启计时(它是键入复位反馈)。那么这一内一外都要有开关了。
      在外:MyButton通过开启自己的计时开关(我叫它外开关),调用内开关,把检测的时间范围送给内开关。在内:MyTimer接收外开关送来的数据并把它赋值给duration,并开启真正的计时开关(内开关)。
      在MyTimer.cs里,真正的计时开关是把计时状态设为RUN,这时switch才会进入到RUN并计时(累积deltaTime)

        public void Go(float _duration){
            elapsedTime = 0;
            duration = _duration;
            state = STATE.RUN;    //这才是真正的内开关
        }
    

      在MyButton.cs里,外开关要做的就是接收指定的计时器和检测时间范围。

        private void StartTimer(MyTimer curTimer, float duration){
            curTimer.Go (duration);
        }
    

      另外我们要在MyButton.cs里宣告两个计时器对象,一个实现Double Trigger,另一个实现Long Press。因为两者的计时区域不一样,所以要两个计时器对象。

        private MyTimer exitTimer = new MyTimer ();    //DoubleTrigger
        private MyTimer delayTimer = new MyTimer ();    //LongPress
    

      为了方便我之后进行测试,我现在就来解释Double Trigger和Long Press的计时区域(计时范围)在哪。在本节的开头已经有所提及:Double Trigger是短时间内快速键入同一个按钮才触发相应的动作,那么它的计时就应该在松开某个指定键开始,直到超过计时范围停止计时,在计时过程中,如果再次键入了同一个按钮,就视为触发Double Trigger。图中虚线区域就是计时范围(duration)。

    Double Trigger
      Long Press是即按住某个按键超过判断的时间范围才会触发相应的动作,即它的计时应该在键入指定按钮的那一刻开始,如果超过了计时范围,即视为玩家的意图是长按,触发Long Press。
    Long Press
      现在清楚了,于Double Trigger而言,开关StartTimer()应放在OnReleased为true之后;于Long Press而言,开关StartTimer()应放在OnPressed为true之后。另外要注意的是,MyButton.cs的Tick()函数是在JoystickInut.cs的Update()函数里被调用才得以推动自身状态的更新,而对于MyTimer.cs的Tick()函数,也要做同样的事情,不然在没人调用Tick()函数的情况下,计时器相当于停滞不前了。我们可以用MyButton的Tick()函数带动MyTimer的Tick()函数。
        public void Tick(bool input){
    
            exitTimer.Tick ();    //带动
    
            curState = input;
            IsPressing = curState;
    
            OnPressed = false;
            OnReleased = false;
    
            if (curState != lastState) {
                if (curState == true) {
                    OnPressed = true;
                    StartTimer (delayTimer, 3.0f);    //测试的是Long Press
                } else {
                    OnReleased = true;
                    //StartTimer (exitTimer, 3.0f);  只是测试函数的逻辑是否正确,一个计时器足矣
                }
            }
            lastState = curState;
        }
    

      现在可以来测试一下了,我在计时器正在计时的时候顺便打印一点信息,以便测试计时器能否正常计时,且当计时完毕后也打印一条信息,测试其是否会正常停止计时:

        public void Tick(){
            switch (state) {
            case STATE.IDLE:
                break;
            case STATE.RUN:
                elapsedTime += Time.deltaTime;
                Debug.Log ("RUNing");
                if (elapsedTime > duration) {
                    state = STATE.FINSHED;
                    break;
                } else {
                    break;
                }
            case STATE.FINSHED:
                Debug.Log ("FINSHED");
                state = STATE.IDLE;      //新增,在计时完毕后计时器应回到闲置状态。
                break;
            default:
                break;
            }
        }
    

      用来测试的按钮依旧是手柄的□按键:ButtonA.Tick (Input.GetKey (keyButA));


      我的操作是快速按一下□(即按下就松开),可以看到计时器在我松开按钮之后马上计时,输出了181条信息,然后结束计时,根据Time.deltaTime(帧速一般情况下是1s60帧)和我给的时间范围3s,得出计时器输出的信息数目基本符合计算结果(3 * 60)。即计时器能正常工作,可喜可贺可喜可贺。
      接下来我在MyButton.cs里设置两个bool变量,这两个bool变量负责告知输入控制组件(JoystickInput.cs和PlayerInput.cs)计时器正在计时。
    public class MyButton {
        ...
        public bool IsExtending = false;    //负责Double Trigger
        public bool IsDelaying = false;      //负责Long Press
        ...
    }
    

      这两个变量会在计时器计时的期间为true,否则为false。

    public void Tick(bool input){
    
            exitTimer.Tick ();
            delayTimer.Tick ();
            
            curState = input;
            IsPressing = curState;
    
            OnPressed = false;
            OnReleased = false;
    
            IsExtending = false;
            IsDelaying = false;
    
            if (curState != lastState) {
                if (curState == true) {
                    OnPressed = true;
                    StartTimer (delayTimer, 3.0f);
                } else {
                    OnReleased = true;
                    //StartTimer (delayTimer, 3.0f);
                }
            }
            if (exitTimer.state == MyTimer.STATE.RUN) {
                IsExtending = true;
            }
            if (delayTimer.state == MyTimer.STATE.RUN) {
                IsDelaying = true;
            }
            lastState = curState;
        }
    

      现在是时候对跑跳滚的条件判定进行大刀阔斧的更改了,我们原本的代码是:

    //在JoystickInput.cs里
        void Update () {
            ...
            //角色奔跑
            run = Input.GetButton (keyButB);
    
            //角色跳跃
            jump = Input.GetButtonDown(keyButD);
    
            //角色攻击
            attack = Input.GetButtonDown(keyButLT);
    
            //角色举盾
            defense = Input.GetButton(keyButRB);
        }
    

      现在我们要把这几个功能按键的信号先送入键入复位反馈机制,让其拥有计时器,然后分析它们的条件判定:

    //在JoystickInput.cs里
        private MyButton ButtonA = new MyButton ();
        private MyButton ButtonB = new MyButton ();
        private MyButton ButtonC = new MyButton ();
        private MyButton ButtonD = new MyButton ();
        private MyButton ButtonLT = new MyButton ();
        private MyButton ButtonRT = new MyButton ();
        private MyButton ButtonLB = new MyButton ();
        private MyButton ButtonRB = new MyButton ();
      
        void Update () {
            ButtonA.Tick (Input.GetKey (keyButA));
            ButtonB.Tick (Input.GetKey (keyButB));
            ButtonC.Tick (Input.GetKey (keyButC));
            ButtonD.Tick (Input.GetKey (keyButD));
            ButtonLT.Tick (Input.GetKey (keyButLT));
            ButtonRT.Tick (Input.GetKey (keyButRT));
            ButtonLB.Tick (Input.GetKey (keyButLB));
            ButtonRB.Tick (Input.GetKey (keyButRB));
            ...
        }
    

      在黑魂中实现跑跳滚功能的是○键,那么我认为在键入○键时间少于0.5s的且人物没有移动(移速低于某个值)的情况下是后跳,有移动就是翻滚;如果是大于0.5s就是奔跑。长按过后松开○键的0.15内如再键入○键就是跳跃+翻滚。
      对应的代码就是这样的:

            //角色奔跑
            run = (!ButtonC.IsDelaying && ButtonC.IsPressing) || ButtonC.IsExtending;
    

    !ButtonC.IsDelaying:检测玩家按住○键是否超过0.5s,没超过为false,超过为true
    ButtonC.IsPressing:如果已经超过0.5s(上一为true),这里检测玩家是否有在继续按住○,有就一直奔跑
    ButtonC.IsExtending:如果玩家已经松开○,角色将继续进行0.15s的跑步再停止。

            //角色跳跃
            jump = ButtonC.IsExtending && ButtonC.OnPressed;
    

    ButtonC.IsExtending:在松开○键后开始计时
    ButtonC.OnPressed:如果在计时范围内再次键入○,就是为触发跳跃

            //角色攻击
            attack = ButtonRB.OnPressed;
    
            //角色举盾
            defense = ButtonLB.IsPressing;
    

      这两个不再赘述。下面重点讨论一下翻滚,之前我们触发翻滚动作的条件是:



      因为现在的跳跃有了更严格的限制条件,不是的单纯的接收○键的输入,所以jump已经不适于用来触发翻滚动作了,需要作出更改。在之前我们的动画状态机里就有一个roll的参数,我们可以用它来作为condition。



      但目前这个roll信号是用来检测是否触发落地翻滚的,如果落地速度超过某个阈值就触发:
            if (rigid.velocity.magnitude>7.0f) {
                anim.SetTrigger("roll");
            }
    

      当然现在仍适用,我们仍需要在落地很快的时候触发roll信号,但为了实现一般情况的翻滚,我们需要增加触发它的机会。为此我们要在IUserInput里增加一个bool变量roll:

        [Header("===== State =====")]
        public bool run;
        public bool jump;
        public bool attack;
        public bool defense;
        public bool rool;
    

      在JoystickInput.cs设置它的值:

            //角色翻滚
            roll = ButtonC.OnReleased && ButtonC.IsDelaying;
    

    ButtonC.OnReleased && ButtonC.IsDelaying:检测玩家是否在蓄力计时阶段松开了按钮,如果是就视为玩家的意图是翻滚而不是奔跑。
      在ActorController.cs里修改触发roll参数(动画状态机的参数)的条件:

            if (pi.roll || rigid.velocity.magnitude>7.0f ) {
                anim.SetTrigger("roll");
            }
    

      然后把Conditions修改一哈:




      现在就可以来看看效果了,可以看到现在基本与黑魂的跳跃机制一致了


    相关文章

      网友评论

          本文标题:Unity——#20 Double Trigger & Long

          本文链接:https://www.haomeiwen.com/subject/eeynlctx.html