黑魂复刻游戏的玩家控制器(跳跃)——Unity随手记(2021.3.29)

今天实现的内容:

跳跃信号

要实现跳跃,首先要实现跳跃的输入。跳跃信号是按下的当场触发的一次性触发控制(Trigger Once Signal)。

public class PlayerInput : MonoBehaviour
{
    
    
	...

	// 一次性信号
    public bool jump; //跳跃信号
    public bool lastJump; //记录上一次的jump信号 用于和当前jump信号做比对 理解为是否正在跳跃

	...

	    // Update is called once per frame
    void Update()
    {
    
    
    	...
    	
		// 获取跳跃键输入
        bool newJump = Input.GetKey(keyJump);
        // 这样只有当按下跳跃键时jump会被设置为true 等同于GetKeyDown
        if (newJump != lastJump && newJump)
        {
    
    
            jump = true;
        }
        else
        {
    
    
            jump = false;
        }
        lastJump = newJump;
	
    }
}

以上代码如果图便宜可以直接采用GetKeyDown来省略if判断。if判断的功能就是让jump只有在按下跳跃键的时候(不是按住,也不是松开)才为true。

跳跃动画的应用

动画机配置很简单,新增trigger参数jump来作为跳跃条件。

在这里插入图片描述
动画触发目前也很简单。

        // 跳跃动画
        if(pi.jump) anim.SetTrigger("jump");

跳跃时锁死Input

为了让跳跃时不能再控制角色旋转,我们需要将Input在跳跃时锁死。好在之前我们就已经实现了输入模块的软开关(详见:魂类游戏的玩家输入模块)。现在的问题就是何时开关输入模块,我们将使用动画状态机StateMachineBehaviour脚本来实现控制。关于StateMachineBehaviour脚本可以往下翻到 值得注意的 部分,里面有讲到。通过下图展示的办法添加一个StateMachineBehaviour脚本。
在这里插入图片描述
以下是FSMOnEnter 的代码,作用是通过重载OnStateEnter,在动画状态机执行到挂载该脚本的动画状态时向父物体及自身的所有MonoBehavior发送消息调用名叫msg的方法。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 使用SendMessage来在OnStateEnter时调用animator.gameObject上的其它方法
public class FSMOnEnter : StateMachineBehaviour
{
    
    
    public string[] onEnterMessage;

    // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    
    
        foreach (var msg in onEnterMessage)
        {
    
    
            //animator.gameObject.SendMessage(msg);
            animator.gameObject.SendMessageUpwards(msg);
        }
    }


}

我们的思路就是,在跳跃动画状态开始,系统调用OnStateEnter方法时向animator所在的gameObject发送信息来调用其它脚本的方法OnJumpEnter,再通过OnJumpEnter来关闭输入模块,至于为什么这么做,因为StateMachineBehaviour脚本里面做的事越少越好,否则出问题了不好找。

public class PlayerController : MonoBehaviour
{
    
    
	// ...
	
    // 跳跃时执行的方法 通过FSMOnEnter中的SendMessage调用
    public void OnJumpEnter()
    {
    
    
        // 关闭输入模块
		pi.inputEnabled = false;
    }
}

同理,在跳跃动画状态结束,系统调用OnStateExit时发送信息再将输入模块打开就行。同样我们再做一个StateMachineBehaviour脚本FSMOnExit。结构与FSMOnEnter一致。改为重载OnStateExit方法就行。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 使用SendMessage来在OnStateExit时调用animator.gameObject上的其它方法
public class FSMOnExit : StateMachineBehaviour
{
    
    
    // 要调用的方法列表
    public string[] onExitMessage;

    // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
    override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    
    
        // 对于每一个方法 发送信息调用他们
        foreach (var msg in onExitMessage)
        {
    
    
            //animator.gameObject.SendMessage(msg);
            animator.gameObject.SendMessageUpwards(msg);
        }
    }

}

同样在PlayerController中定义方法OnJumpExit。

public class PlayerController : MonoBehaviour
{
    
    
	// ...
	
    // 跳跃时执行的方法 通过FSMOnExit中的SendMessage调用
    public void OnJumpExit()
    {
    
    
        // 打开输入模块
		pi.inputEnabled = true;
    }
}

在关闭输入模块以后,我们的位移也就终止了,因为我们的软开关是将输入设置为0来达成的,为了保证跳跃时依旧拥有原来的水平方向位移,我们需要在跳跃时将水平方向的位移量锁死,也就是不再计算新的位移量。

public class PlayerController : MonoBehaviour
{
    
    
	// ... 
	
    // 角色的平面位移大小
    private Vector3 planarVec; //以前叫movingVec 改名了
    // 是否锁死平面移动
    private bool lockPlanar = false;

    // Update is called once per frame
    void Update()
    {
    
    
    	// ...	
    	
		// 计算移动量 速度向量
        if(!lockPlanar) planarVec = pi.dirMag * model.transform.forward * walkSpeed * (pi.run ? runMultiplier : 1.0f);
    }

    // 跳跃时执行的方法 通过FSMOnEnter中的SendMessage调用
    public void OnJumpEnter()
    {
    
    
        // 关闭输入模块
        pi.inputEnabled = false;
        lockPlanar = true;
    }

    // 跳跃时执行的方法 通过FSMOnExit中的SendMessage调用
    public void OnJumpExit()
    {
    
    
        // 打开输入模块
        pi.inputEnabled = true;
        lockPlanar = false;
    }
}

这样一来,跳跃开始时planarVec的值就会一直保持为上一帧的值,直到跳跃结束才开始计算新值。

跳跃冲量

为了让角色在跳跃时能够真的跳起来,我们需要给他一个向上的冲量。

public class PlayerController : MonoBehaviour
{
    
    
	// ... 
	
	// 跳跃速度
    public float jumpVelocity = 5.0f;
	
	// 角色的跳跃冲量
	private Vector3 thrustVec;

    
    // 处理刚体的操作
    private void FixedUpdate()
    {
    
    
        // 直接修改position实现位移
        rigidbody.position += planarVec * Time.fixedDeltaTime;
        rigidbody.velocity += thrustVec;
        thrustVec = Vector3.zero;
    }

    // 跳跃时执行的方法 通过FSMOnEnter中的SendMessage调用
    public void OnJumpEnter()
    {
    
    
        // ...
        // 计算跳跃的向上冲量
		thrustVec.y = jumpVelocity;

    }
}

BUG以及缺陷:

当你快速连续按两次跳跃键,跳跃也会紧挨着触发两次。也就是说,trigger会累积起来。要处理这个问题,我们可以通过在动画节点上添加一个新的StateMachineBehaviour脚本。
在这里插入图片描述
接下来,加入代码,在每次进入到ground动画状态时reset掉跳跃状态的trigger。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 用于Trigger信号的清除工作
public class FSMClearSignals : StateMachineBehaviour
{
    
    
    // 进入该动画状态时需要清空的信号
    public string[] signalsClearAtEnter;
    
    ...
    
    // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    
    
        foreach (var signal in signalsClearAtEnter)
        {
    
    
            animator.ResetTrigger(signal);
        }
    }
	
	...
}

值得注意的:

对于StateMachineBehaviour脚本,我们可以重载它提供的几个方法,OnStateEnter会在刚开始执行这个动画状态时调用,OnStateUpdate会在当前动画状态正在执行的每一个Update帧调用,OnStateExit会在动画状态结束时调用。还有一些别的方法。
在这里插入图片描述
这里我们写了一个脚本FSMClearSignals,专门用于在进入、离开某个状态时清除trigger,我们以后还会使用到这个脚本。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 用于Trigger信号的清除工作
public class FSMClearSignals : StateMachineBehaviour
{
    
    
    // 进入该动画状态时需要清空的信号
    public string[] signalsClearAtEnter;
    // 离开该动画状态时需要清空的信号
    public string[] signalsClearAtExit;
    
    // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    
    
        foreach (var signal in signalsClearAtEnter)
        {
    
    
            animator.ResetTrigger(signal);
        }
    }

    // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
    override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
    
    
        foreach (var signal in signalsClearAtExit)
        {
    
    
            animator.ResetTrigger(signal);
        }
    }

}


动画状态机下挂载的StateMachineBehaviour脚本最好少而简单,否则一旦出现Bug会很难抓。这就是为什么我们在实现锁死Input时只是用FSMOnEnter来SendMessage,而不是直接用一个StateMachineBehaviour脚本把事情做完了。


我们在动画状态机脚本中使用SendMessage其实性能不太好,后期有优化的余地。


猜你喜欢

转载自blog.csdn.net/qq_37856544/article/details/115298300