[Unity] 战斗系统学习 6:构建 TPS 框架 2

1. Port 与 BBParameter

我一开始就是公开组件接口,那个时候是因为我还只会用 AddValueInput,不会用 BBParameter
但是后来我发现 BBParameter 很简单之后,我就想在每个地方都用上 BBParameter 了,因为我希望连线尽可能少
但是后面我又发现其实直接用接口的话,他会设置默认的 self 传进来,这样其实对于有默认值的接口,也不用连很多线
那么把这个接口暴露出来的意义就是,让人知道明确知道有这么一个东西需要连,一眼看过去就知道,而不需要点开这个节点看需要什么黑板值
而黑板值用在一些具体细节的布置上,比如我摄像机的偏移为多少,角色速度调到多少,很多时候这些都是一个死的东西,不需要连线,也不需要黑板上有值,给个字面值就可以了的这种

但是由于 FlowCanvas 只会对多 Component 接口的第一个接口设置默认的 self,这就强迫设计者一个函数只能用一个组件
某种程度上也算是强迫解耦了?

1.1 冲刺 v6

[Unity] 战斗系统学习 6:构建 TPS 框架 2_第1张图片

例如原来我会把禁止玩家输入和速度覆盖放在一起,然后用两个组件,然后这两个组件都是黑板值,现在我把组件接口暴露出来,然后解耦了禁止输入和速度覆盖

2. 自定义节点与公有函数

一开始我想什么都用自定义节点
但是我发现了单一组件原则之后,对比自定义节点与公有函数,它们都是处理一个组件内的事情,那为什么不直接在组件里面写函数呢?
组件内写函数反而方便在 FlowScript 中组合
只有对于那些真的很基础的节点才需要自定义节点,比如我想要给 wait 添加一个时间缩放啥的,不得不新建一个带缩放因子变量的节点

2.1 冲刺 v7

[Unity] 战斗系统学习 6:构建 TPS 框架 2_第2张图片

比如我原来会把速度覆盖与计时放在一起,现在想想确实没必要,新建一个这样的节点浪费工作量

3. LatentActionNode 的 Break

LatentActionNode 的 Break 会直接触发 Finish

3.1 瞄准 v4

[Unity] 战斗系统学习 6:构建 TPS 框架 2_第3张图片

我原来是想在协程结束之后把瞄准图标 SetActive(false)
但是 Break 会直接触发协程的 FinishFinish 又会触发 SetActive(false),因此这就相当于我要显示图标的同时把他 SetActive(true) SetActive(false),这就很迷惑

[Unity] 战斗系统学习 6:构建 TPS 框架 2_第4张图片

所以干脆不要这个 SetActive 了,直接一直为 true

4. 运动模式

4.1 瞄准 v5

后面我又觉得其实没有必要特意把一个 CanvasGroup 连出来
于是瞄准变成这样

[Unity] 战斗系统学习 6:构建 TPS 框架 2_第5张图片

在这里我使用了第二个 set style 然后我还注意到所有的功能都是迫不得已一个组件的
然后接下来我又发现 locomotion 的计算也需要改变,因为我的基本运动是人物会朝着摄像机方向+输入方向的,持枪的时候人物应该始终朝着前面,所以人物移动应该是朝着摄像机的前方向和右方向的
这就要求我再在一个地方注意更改
这样我就想到,为什么我不给一个组件做一个模式改变的函数呢?模式改变所需要的参数,模式改变具体要用的函数,感觉也不需要在运行时用到,暴露出来也没有意义

4.2 瞄准 v6

于是修改之后就变成这样

[Unity] 战斗系统学习 6:构建 TPS 框架 2_第6张图片

看上去就很简洁

4.3 Locomotion v4

Assets/MeowFramework/TPSCharacter/Scripts/TPSCharacterLocomotionController.cs

// ----------------------------------------------
// 作者: 廉价喵
// 创建于: 16/03/2022 16:53
// 最后一次修改于: 10/04/2022 22:09
// 版权所有: CheapMeowStudio
// 描述:
// ----------------------------------------------

using System.Collections;
using System.ComponentModel;
using Cinemachine;
using MeowFramework.Core;
using Sirenix.OdinInspector;
using UnityEngine;

namespace MeowFramework.TPSCharacter
{
	/// 
	/// 第三人称运动控制器
	/// 
    public class TPSCharacterLocomotionController : SerializedMonoBehaviour
    {
	    // 常量
	    
	    /// 
	    /// 微量
	    /// 
	    private const float Threshold = 0.01f;
	    
	    // 组件相关

	    /// 
	    /// 角色控制器
	    /// 
	    [BoxGroup("Component")]
	    [Required]
	    [Tooltip("角色控制器")]
	    public CharacterController CharacterCtr;

	    /// 
	    /// ACT 输入控制器
	    /// 
	    [BoxGroup("Component")]
	    [Required]
	    [Tooltip("ACT 输入控制器")]
	    public TPSCharacterInputController ACTInput;
	    
	    /// 
	    /// 摄像机跟随点
	    /// 
	    [BoxGroup("Component")]
	    [Required]
	    [Tooltip("摄像机跟随点")]
	    public GameObject CMCameraFollowTarget;

	    /// 
	    /// 主摄像机
	    /// 
	    [BoxGroup("Component")]
	    [Sirenix.OdinInspector.ReadOnly]
	    [Tooltip("主摄像机")]
	    public Camera MainCamera;
	    
	    /// 
	    /// 跟随主角的摄像机
	    /// 
	    [BoxGroup("Component")]
	    [Sirenix.OdinInspector.ReadOnly]
	    [Tooltip("跟随主角的摄像机")]
	    public CinemachineVirtualCamera PlayerFollowCamera;
	    
	    // 模式
	    
	    /// 
	    /// 行动模式
	    /// 
	    [BoxGroup("Mode")]
	    [ShowInInspector]
	    [Sirenix.OdinInspector.ReadOnly]
	    [Description("行动模式")]
	    private TPSCharacterBehaviourMode mode;

	    /// 
	    /// 切换模式的过渡时间
	    /// 
	    [BoxGroup("Mode")]
	    [ShowInInspector]
	    [Description("切换模式的过渡时间")]
	    private float modeTransitionTime = 1f;

	    /// 
	    /// 没有武器时摄像机的 FOV
	    /// 
	    [BoxGroup("Mode")]
	    [ShowInInspector]
	    [Description("摄像机的目标 FOV")]
	    private float noWeaponFOV = 40f;
	    
	    /// 
	    /// 持步枪时摄像机的 FOV
	    /// 
	    [BoxGroup("Mode")]
	    [ShowInInspector]
	    [Description("摄像机的目标 FOV")]
	    private float rifleFOV = 30f;
	    
	    /// 
	    /// 摄像机的 FOV 的平滑时间
	    /// 
	    [BoxGroup("Mode")]
	    [ShowInInspector]
	    [Description("摄像机的目标 FOV 的平滑时间")]
	    private float fovSmoothTime = 0.2f;
        
	    /// 
	    /// 没有武器时摄像机的侧向位置
	    /// 
	    [BoxGroup("Mode")]
	    [ShowInInspector]
	    [Description("摄像机的目标侧向位置")]
	    private float noWeaponSide = 0.5f;

	    /// 
	    /// 持步枪时摄像机的侧向位置
	    /// 
	    [BoxGroup("Mode")]
	    [ShowInInspector]
	    [Description("摄像机的目标侧向位置")]
	    private float rifleSide = 1f;
	    
	    /// 
	    /// 摄像机侧向位置的平滑时间
	    /// 
	    [BoxGroup("Mode")]
	    [ShowInInspector]
	    [Description("摄像机侧向位置的平滑时间")]
	    private float cameraSideSmoothTime = 0.2f;
	    
	    // 运动相关
	    
	    /// 
	    /// 可资产化水平速度
	    /// 
	    [BoxGroup("Velocity")]
	    [Required]
	    [Tooltip("可资产化水平速度")]
	    public ScriptableVector3Variable HorizontalVelocity;

	    /// 
	    /// 可资产化竖直速度
	    /// 
	    [BoxGroup("Velocity")]
	    [Required]
	    [Tooltip("可资产化竖直速度")]
	    public ScriptableFloatVariable VerticalVelocity;

	    /// 
	    /// 水平速度是否被覆盖了
	    /// 
	    [BoxGroup("Velocity")]
	    [HorizontalGroup("Velocity/HorizontalVelocityOverride")]
	    [ShowInInspector]
	    [Sirenix.OdinInspector.ReadOnly]
	    [Tooltip("水平速度覆盖值")]
	    private bool isHorizontalVelocityOverrided;
	    
	    /// 
	    /// 水平速度覆盖值
	    /// 
	    [BoxGroup("Velocity")]
	    [HorizontalGroup("Velocity/HorizontalVelocityOverride")]
	    [ShowInInspector]
	    [Sirenix.OdinInspector.ReadOnly]
	    [Tooltip("水平速度覆盖值")]
	    private float horizontalVelocityOverride;

	    // 行走相关

	    /// 
	    /// 移动速度
	    /// 
	    [BoxGroup("Walk")]
	    [ShowInInspector]
	    [Tooltip("移动速度")]
	    private float walkSpeed = 7f;

	    /// 
	    /// 玩家行走的过渡时间
	    /// 
	    [BoxGroup("Walk")]
	    [ShowInInspector]
	    [Tooltip("玩家行走的过渡时间")]
	    private float walkSmoothTime = 0.2f;

	    /// 
	    /// 玩家旋转的过渡时间
	    /// 如果这个值过大,由于使用了 SmoothDamp,会让镜头移动出现明显的粘滞感
	    /// 
	    [BoxGroup("Walk")]
	    [ShowInInspector]
	    [Tooltip("玩家旋转的过渡时间")]
	    private float rotationSmoothTime = 0.1f;

	    // 物理相关
	    
	    /// 
	    /// 重力系数
	    /// 
	    [BoxGroup("Gravity")]
	    [ShowInInspector]
	    [Tooltip("重力系数")]
	    private float gravity = -9.8f;
	    
	    /// 
	    /// 最大下落速度
	    /// 
	    [BoxGroup("Gravity")]
	    [ShowInInspector]
	    [Tooltip("最大下落速度")]
	    private float terminalVelocity = 53f;
	    
	    // 落地相关

	    /// 
	    /// 是否落地
	    /// 
	    [BoxGroup("GroundCheck")]
	    [ShowInInspector]
	    [Sirenix.OdinInspector.ReadOnly]
	    [Tooltip("是否落地")]
	    private bool isGrounded;

	    /// 
	    /// 是否落地
	    /// 
	    public bool IsGrounded => isGrounded;
	    
	    /// 
	    /// 落地球形碰撞检测中心点的竖向偏移量
	    /// 
	    [BoxGroup("GroundCheck")]
	    [ShowInInspector]
	    [Tooltip("落地球形碰撞检测中心点的竖向偏移量")]
	    private float groundedOffset = -0.14f;
	    
	    /// 
	    /// 落地球形碰撞检测的半径
	    /// 
	    [BoxGroup("GroundCheck")]
	    [ShowInInspector]
	    [Tooltip("落地球形碰撞检测的半径")]
	    private float groundedRadius = 0.28f;
	    
	    /// 
	    /// 落地球形碰撞检测的层级
	    /// 
	    [BoxGroup("GroundCheck")]
	    [ShowInInspector]
	    [Tooltip("落地球形碰撞检测的层级")]
	    private int groundLayers = 1;

	    // 摄像机相关
		
	    /// 
	    /// 摄像机是否固定
	    /// 
	    [BoxGroup("Camera")]
	    [ShowInInspector]
	    [Sirenix.OdinInspector.ReadOnly]
	    [Tooltip("摄像机是否固定")]
	    private bool IsCameraFixed = false;
	    
	    /// 
	    /// 摄像机俯仰角覆盖值
	    /// 
		[BoxGroup("Camera")]
		[ShowInInspector]
	    [Tooltip("摄像机俯仰角覆盖值")]
	    private float cameraPitchOverride = 0f;
		
	    /// 
	    /// 摄像机转动速度
	    /// 
	    [BoxGroup("Camera")]
	    [ShowInInspector]
	    [Tooltip("摄像机转动速度")]
	    private float cameraRotSpeed = 25f;
	    
	    /// 
	    /// 摄像机最大俯仰角
	    /// 
	    [BoxGroup("Camera")]
	    [PropertyRange(0,90)]
	    [ShowInInspector]
	    [Tooltip("摄像机最大俯仰角")]
	    private float topClamp = 70f; 
	    
	    /// 
	    /// 摄像机最小俯仰角
	    /// 
	    [BoxGroup("Camera")]
	    [PropertyRange(-90,0)]
	    [ShowInInspector]
	    [Tooltip("摄像机最小俯仰角")]
	    private float bottomClamp = -30f;
	    
	    /// 
	    /// 摄像机跟随点的当前俯仰角的过渡时间
	    /// 
	    [BoxGroup("Camera")]
	    [ShowInInspector]
	    [Tooltip("摄像机跟随点的当前俯仰角的过渡时间")]
	    private float cinemachinePitchSmoothTime = 0.1f;
	    
	    /// 
	    /// 摄像机跟随点的当前偏航角的过渡时间
	    /// 
	    [BoxGroup("Camera")]
	    [ShowInInspector]
	    [Tooltip("摄像机跟随点的当前偏航角的过渡时间")]
	    private float cinemachineYawSmoothTime = 0.1f;

	    // 缓存
	    
	    // 缓存 - 速度覆盖
	    
	    /// 
	    /// 水平速度方向覆盖值
	    /// 
	    private Vector3 horizontalVelocityDirectionOverride;
	    
	    // 缓存 - 玩家行走
	    
	    /// 
	    /// 行走速度的过渡速度
	    /// 
	    private Vector3 walkSmoothVelocity;
	    
	    /// 
	    /// 旋转角的过渡速度
	    /// 
	    private float rotationSmoothVelocity;
	    
	    // 缓存 - 摄像机旋转
	    
	    /// 
	    /// 摄像机跟随点的期望俯仰角
	    /// 
	    private float cinemachineTargetPitch;
	    
	    /// 
	    /// 摄像机跟随点的期望偏航角
	    /// 
	    private float cinemachineTargetYaw;
	    
	    /// 
	    /// 摄像机跟随点的当前俯仰角和摄像机跟随点的当前偏航角组成的向量
	    /// 
	    private Vector2 cinemachineCurrPY;
	    
	    /// 
	    /// 摄像机跟随点的当前俯仰角的过渡速度
	    /// 
	    private float cinemachinePitchSmoothVelocity;
	    
	    /// 
	    /// 摄像机跟随点的当前俯仰角的过渡速度
	    /// 
	    private float cinemachineYawSmoothVelocity;
	    
	    // 缓存 - 运动模式

	    /// 
	    /// 模式改变协程
	    /// 
	    private Coroutine modeChangeCoroutine;
	    
	    /// 
	    /// 摄像机的目标 FOV 平滑速度
	    /// 
	    private float fovSmoothVelocity;
	    
	    /// 
	    /// 摄像机侧向位置的平滑速度
	    /// 
	    private float cameraSideSmoothVelocity;
	    
	    public void Awake()
	    {
		    MainCamera = GameObject.FindGameObjectWithTag("MainCamera").GetComponent<Camera>();
		    PlayerFollowCamera = GameObject.Find("PlayerFollowCamera").GetComponent<CinemachineVirtualCamera>();
	    }

	    public void Start()
	    {
		    PlayerFollowCamera.Follow = CMCameraFollowTarget.transform;
		    
		    // 之所以不使用订阅委托的方式调用 Move RotateToMoveDir CameraRotate
		    // 是因为他们有着 Update LateUpdate 的先后顺序要求
		    // 同时平滑功能也要求它们是每帧调用的
	    }

	    protected void Update()
	    {
		    ApplyGravity();
		    GroundedCheck();
		    Move();
	    }

	    protected void LateUpdate()
	    {
		    CameraRotate();
	    }
	    
	    /// 
	    /// 落地检查
	    /// 
	    private void GroundedCheck()
        {
	        var spherePosition = new Vector3(transform.position.x, transform.position.y - groundedOffset, transform.position.z);
            isGrounded = Physics.CheckSphere(spherePosition, groundedRadius, groundLayers, QueryTriggerInteraction.Ignore);
        }
	    
	    /// 
	    /// 应用重力
	    /// 
        private void ApplyGravity()
        {
	        if (isGrounded && VerticalVelocity.Value < 0.0f)
		        VerticalVelocity.Value = -2f;
	        else if (VerticalVelocity.Value < terminalVelocity)
		        VerticalVelocity.Value += gravity * Time.deltaTime;
        }

        /// 
        /// 移动
        /// 
        private void Move()
        {
	        HorizontalVelocity.Value = GetSpeed();

	        CharacterCtr.Move((HorizontalVelocity.Value + VerticalVelocity.Value * new Vector3(0,1,0)) * Time.deltaTime);
        }

        private Vector3 GetSpeed()
        {
	        // 基于摄像机方向,向键盘输入方向移动的方向
	        Vector3 targetDirection = GetInputDirectionBaseOnCamera();

	        RotateToMoveDir(targetDirection);

			// 如果有速度覆盖,则直接返回速度覆盖的结果
	        float targetSpeed = isHorizontalVelocityOverrided ? horizontalVelocityOverride : walkSpeed;
	        
	        // 如果没有速度覆盖,则返回 Smooth 的结果
	        Vector3 targetVelocity = (ACTInput.Move == Vector2.zero && isHorizontalVelocityOverrided == false) ? Vector3.zero : targetDirection * targetSpeed;
	        
	        return Vector3.SmoothDamp(HorizontalVelocity.Value, targetVelocity, ref walkSmoothVelocity, walkSmoothTime);
        }

        /// 
        /// 向移动方向旋转
        /// 
        private void RotateToMoveDir(Vector3 inputDirection)
        {
	        // 有键盘输入
	        // 或者没有键盘输入但是有速度覆盖时
	        // 才会启用旋转
	        if (ACTInput.Move != Vector2.zero || isHorizontalVelocityOverrided == true)
	        {
		        float targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg;
		        float rotation = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetRotation, ref rotationSmoothVelocity, rotationSmoothTime);

		        // 玩家旋转
		        transform.rotation = Quaternion.Euler(0.0f, rotation, 0.0f);
	        }
        }

        /// 
        /// 摄像机旋转
        /// 
        private void CameraRotate()
        {
	        // if there is an input and camera position is not fixed
	        if (ACTInput.Look.sqrMagnitude >= Threshold && !IsCameraFixed)
	        {
		        cinemachineTargetPitch += ACTInput.Look.y * Time.deltaTime * cameraRotSpeed / 100.0f;
		        cinemachineTargetYaw += ACTInput.Look.x * Time.deltaTime * cameraRotSpeed / 100.0f;
	        }

	        // clamp our rotations so our values are limited 360 degrees
	        cinemachineTargetPitch = MathUtility.ClampAngle(cinemachineTargetPitch, bottomClamp, topClamp);
	        cinemachineTargetYaw = MathUtility.ClampAngle(cinemachineTargetYaw, float.MinValue, float.MaxValue);

	        // 平滑
	        cinemachineCurrPY.x = Mathf.SmoothDampAngle(cinemachineCurrPY.x, cinemachineTargetPitch,
		        ref cinemachinePitchSmoothVelocity, cinemachinePitchSmoothTime);
	        cinemachineCurrPY.y = Mathf.SmoothDampAngle(cinemachineCurrPY.y, cinemachineTargetYaw,
		        ref cinemachineYawSmoothVelocity, cinemachineYawSmoothTime);
	  
	        // Cinemachine will follow this target
	        CMCameraFollowTarget.transform.rotation = Quaternion.Euler(cinemachineCurrPY.x + cameraPitchOverride, cinemachineCurrPY.y, 0.0f);
        }

        /// 
        /// 基于摄像机方向,向键盘输入方向移动的方向
        /// 
        /// 
        private Vector3 GetInputDirectionBaseOnCamera()
        {
	        // 输入移动方向
	        Vector3 inputDirection = new Vector3(ACTInput.Move.x, 0.0f, ACTInput.Move.y).normalized;

	        // 期望旋转
	        // 因为摄像机呼吸,MainCamera.transform.eulerAngles.y 会发生抖动,进而导致玩家在起步的时候有一个微小抖动
	        // 而 cinemachineTargetYaw 不会抖动,因此采用 cinemachineTargetYaw
	        float targetRotation = Mathf.Atan2(inputDirection.x, inputDirection.z) * Mathf.Rad2Deg + cinemachineTargetYaw;
	        // 基于摄像机方向,向键盘输入方向移动的方向
	        Vector3 targetDirection = Quaternion.Euler(0.0f, targetRotation, 0.0f) * Vector3.forward;

	        return targetDirection;
        }
        
        /// 
        /// 取消速度覆盖
        /// 
        public void CancelHorizontalVelocityOverride()
        {
	        isHorizontalVelocityOverrided = false;
        }
        
        /// 
        /// 默认速度覆盖方向为当前摄像机方向+键盘输入方向
        /// 
        /// 速度
        public void SetHorizontalVelocityOverride(float speed)
        {
	        isHorizontalVelocityOverrided = true;
	        horizontalVelocityOverride = speed;
        }
        
        // 后面这个可能会有强制初始方向和强制始终一个方向
        // 现在是懒得做了
        
        // /// 
        // /// 允许设置速度覆盖方向
        // /// 
        // /// 速度
        // /// 速度方向
        // public void SetHorizontalVelocityOverride(float speed, Vector3 dir)
        // {
	       //  isHorizontalVelocityOverrided = true;
	       //  HorizontalVelocityOverride = speed;
	       //  HorizontalVelocityDirectionOverride = dir;
        // }

        /// 
        /// 开始模式改变的协程函数
        /// 
        /// 目标 FOV
        /// 目标侧向位置
        /// 
        private IEnumerator StartModeChange(float targetFOV, float targetSide)
        {
	        // 摄像机第三人称跟随组件
	        var camera3rdPersonFollow =
		        PlayerFollowCamera.GetCinemachineComponent<Cinemachine3rdPersonFollow>();

	        // 初始化计时器
	        var timeLeft = modeTransitionTime;
            
	        // 在给定时间内平滑
	        // 平滑时间结束时,被平滑项接近终点值但不是终点值
	        // 因此最后需要给被平滑项赋终点值,这可能产生一个抖动
	        // 因此平滑时间需要在保证效果的同时尽可能小,才能让最后的抖动变小
	        while (timeLeft > 0)
	        {
		        timeLeft -= Time.deltaTime;
		        PlayerFollowCamera.m_Lens.FieldOfView = Mathf.SmoothDamp(PlayerFollowCamera.m_Lens.FieldOfView,
			        targetFOV, ref fovSmoothVelocity, fovSmoothTime);
		        camera3rdPersonFollow.CameraSide = Mathf.SmoothDamp(camera3rdPersonFollow.CameraSide, targetSide,
			        ref cameraSideSmoothVelocity, cameraSideSmoothTime);
		        
		        yield return null;
	        }
	        
	        // 摄像机焦距设置赋终点值
	        PlayerFollowCamera.m_Lens.FieldOfView = targetFOV;
	        // 摄像机侧向位置赋终点值
	        camera3rdPersonFollow.CameraSide = targetSide;
            
	        yield return null;
	        
        }
        
        /// 
        /// 改变运动模式
        /// 
        /// 模式
        public void SetLocomotionMode(TPSCharacterBehaviourMode mode)
        {
	        this.mode = mode;
	        if(modeChangeCoroutine != null)
				StopCoroutine(modeChangeCoroutine);
	        switch (mode)
	        {
		        case TPSCharacterBehaviourMode.NoWeapon:
			        modeChangeCoroutine = StartCoroutine(StartModeChange(noWeaponFOV, noWeaponSide));
			        break;
		        case TPSCharacterBehaviourMode.Rifle:
			        modeChangeCoroutine = StartCoroutine(StartModeChange(rifleFOV, rifleSide));
			        break;
	        }
        }
    }
}


4.4 AnimationController v4

Assets/MeowFramework/TPSCharacter/Scripts/TPSCharacterAnimationController.cs

// ----------------------------------------------
// 作者: 廉价喵
// 创建于: 25/03/2022 23:24
// 最后一次修改于: 10/04/2022 22:15
// 版权所有: CheapMeowStudio
// 描述:
// ----------------------------------------------

using System;
using System.Collections;
using System.ComponentModel;
using MeowFramework.Core;
using Sirenix.OdinInspector;
using Unity.Collections;
using UnityEngine;

namespace MeowFramework.TPSCharacter
{
    /// 
    /// 第三人称动画状态机控制器
    /// 
    public class TPSCharacterAnimationController : SerializedMonoBehaviour
    {
        // 组件

        /// 
        /// 第三人称运动管理器
        /// 
        [BoxGroup("Component")]
        [Required]
        [Tooltip("第三人称运动管理器")]
        public TPSCharacterLocomotionController LocomotionController;
        
        /// 
        /// 动画控制器
        /// 
        [BoxGroup("Component")]
        [Required]
        [Tooltip("动画控制器")]
        public Animator Anim;

        /// 
        /// 行动模式
        /// 
        [BoxGroup("Mode")]
        [ShowInInspector]
        [Sirenix.OdinInspector.ReadOnly]
        [Description("行动模式")]
        private TPSCharacterBehaviourMode mode;

        /// 
        /// 切换模式的过渡时间
        /// 
        [BoxGroup("Mode")]
        [ShowInInspector]
        [Description("切换模式的过渡时间")]
        private float modeTransitionTime = 1f;

        /// 
        /// 层级平滑时间
        /// 
        [BoxGroup("Mode")]
        [ShowInInspector]
        [Description("层级平滑时间")]
        private float layerWeightSmoothTime = 0.2f;
        
        /// 
        /// 向前的速度
        /// 
        [BoxGroup("AnimatorParameter")]
        [Sirenix.OdinInspector.ReadOnly]
        [ShowInInspector]
        [Description("向前的速度")]
        private float forwardSpeed;

        /// 
        /// 向右的速度
        /// 
        [BoxGroup("AnimatorParameter")]
        [Sirenix.OdinInspector.ReadOnly]
        [ShowInInspector]
        [Description("向右的速度")]
        private float rightSpeed;

        // 缓存
        
        // 缓存 - 动画机参数计算
        
        /// 
        /// 摄像机的前方
        /// 
        private Vector3 cameraForward;

        /// 
        /// 摄像机的右方
        /// 
        private Vector3 cameraRight;
        
        // 缓存 - id
	    
        /// 
        /// 动画状态机的摄像机前方速度参数的 id
        /// 
        private int animIDForwardSpeed;
        
        /// 
        /// 动画状态机的摄像机右方速度参数的 id
        /// 
        private int animIDRightSpeed;
        
        /// 
        /// 动画状态机的落地参数的 id
        /// 
        private int animIDGrounded;
        
        /// 
        /// 动画状态机的自由落体参数的 id
        /// 
        private int animIDFreeFall;
        
        /// 
        /// 动画状态机的近战攻击参数的 id
        /// 
        private int animIDMeleeAttack;
        
        // 缓存 - 模式改变

        /// 
        /// 模式改变协程
        /// 
        private Coroutine modeChangeCoroutine;
        
        /// 
        /// 旧层级权重平滑速度
        /// 
        private float fromLayerWeightSmoothVelocity;
        
        /// 
        /// 新层级权重平滑速度
        /// 
        private float toLayerWeightSmoothVelocity;
        
        public void Start()
        {
            AssignAnimationIDs();
        }

        public void Update()
        {
            SetAnimatorValue();
        }

        /// 
        /// 初始化动画状态机参数
        /// 
        public void AssignAnimationIDs()
        {
            animIDForwardSpeed = Animator.StringToHash("ForwardSpeed");
            animIDRightSpeed = Animator.StringToHash("RightSpeed");
            animIDGrounded = Animator.StringToHash("Grounded");
            animIDFreeFall = Animator.StringToHash("FreeFall");
            animIDMeleeAttack = Animator.StringToHash("Attack");
        }

        /// 
        /// 设置动画状态机参数
        /// 
        public void SetAnimatorValue()
        {
            // 着地
            Anim.SetBool(animIDGrounded, LocomotionController.IsGrounded);
            
            // 跳跃
            if (LocomotionController.IsGrounded)
                Anim.SetBool(animIDFreeFall, false);
            // 自由下落
            else
                Anim.SetBool(animIDFreeFall, true);

            // 移动
            if (mode == TPSCharacterBehaviourMode.NoWeapon)
            {
	            forwardSpeed = LocomotionController.HorizontalVelocity.Value.magnitude;

	            Anim.SetFloat(animIDForwardSpeed, forwardSpeed);
            }
            else if (mode == TPSCharacterBehaviourMode.Rifle)
            {
	            cameraForward = Camera.main.transform.forward;
	            cameraRight = Camera.main.transform.right;
	            cameraForward.y = 0;
	            forwardSpeed = Vector3.Dot(LocomotionController.HorizontalVelocity.Value, cameraForward);
	            rightSpeed = Vector3.Dot(LocomotionController.HorizontalVelocity.Value, cameraRight);

	            Anim.SetFloat(animIDForwardSpeed, forwardSpeed);
	            Anim.SetFloat(animIDRightSpeed, rightSpeed);
            }
        }

        private void OnBeginMeleeAttack()
        {
            Anim.SetTrigger(animIDMeleeAttack);
        }

        /// 
        /// 开始模式改变的协程函数
        /// 
        /// 目标 FOV
        /// 目标侧向位置
        /// 
        private IEnumerator StartModeChange(int fromLayer, int toLayer)
        {
	        // 初始化计时器
	        var timeLeft = modeTransitionTime;

	        // 层级
	        var fromWeight = Anim.GetLayerWeight(fromLayer);
	        var toWeight = Anim.GetLayerWeight(toLayer);
	        
	        // 在给定时间内平滑
	        // 平滑时间结束时,被平滑项接近终点值但不是终点值
	        // 因此最后需要给被平滑项赋终点值,这可能产生一个抖动
	        // 因此平滑时间需要在保证效果的同时尽可能小,才能让最后的抖动变小
	        while (timeLeft > 0)
	        {
		        timeLeft -= Time.deltaTime;
		        fromWeight = Mathf.SmoothDamp(fromWeight, 0,
			        ref fromLayerWeightSmoothVelocity, layerWeightSmoothTime);
		        toWeight = Mathf.SmoothDamp(toWeight, 1,
			        ref toLayerWeightSmoothVelocity, layerWeightSmoothTime);
		        Anim.SetLayerWeight(fromLayer, fromWeight);
		        Anim.SetLayerWeight(toLayer, toWeight);
		        yield return null;
	        }
	        
	        // 赋终点值
	        Anim.SetLayerWeight(fromLayer, 0);
	        Anim.SetLayerWeight(toLayer, 1);
            
	        yield return null;
	        
        }
        
        /// 
        /// 设置动画模式
        /// 
        /// 模式
        public void SetAnimationMode(TPSCharacterBehaviourMode mode)
        {
	        this.mode = mode;
	        if(modeChangeCoroutine != null)
				StopCoroutine(modeChangeCoroutine);
	        switch (mode)
	        {
		        case TPSCharacterBehaviourMode.NoWeapon:
			        modeChangeCoroutine = StartCoroutine(StartModeChange(1, 0));
			        break;
		        case TPSCharacterBehaviourMode.Rifle:
			        modeChangeCoroutine = StartCoroutine(StartModeChange(0, 1));
			        break;
	        }
        }
    }
}

4.5 UIController v4

Assets/MeowFramework/TPSCharacter/Scripts/TPSCharacterUIController.cs

// ----------------------------------------------
// 作者: 廉价喵
// 创建于: 10/04/2022 14:04
// 最后一次修改于: 10/04/2022 22:09
// 版权所有: CheapMeowStudio
// 描述:
// ----------------------------------------------

using System.ComponentModel;
using MeowFramework.Core;
using Sirenix.OdinInspector;
using UnityEngine;

namespace MeowFramework.TPSCharacter
{
    /// 
    /// 第三人称 UI 控制器
    /// 
    public class TPSCharacterUIController : SerializedMonoBehaviour
    {
        // 组件
        
        /// 
        /// 瞄准 UI
        /// 
        [Required]
        [BoxGroup("Component")]
        [Description("瞄准 UI")]
        public GameObject AimUI;
        
        /// 
        /// 血量 UI
        /// 
        [Required]
        [BoxGroup("Component")]
        [Description("血量 UI")]
        public GameObject HPUI;
        
        /// 
        /// 体力条 UI
        /// 
        [Required]
        [BoxGroup("Component")]
        [Description("体力条 UI")]
        public GameObject NRGUI;

        // 模式
        
        /// 
        /// 行动模式
        /// 
        [BoxGroup("Mode")]
        [ShowInInspector]
        [Sirenix.OdinInspector.ReadOnly]
        [Description("行动模式")]
        private TPSCharacterBehaviourMode mode;

        // 缓存
        
        // 缓存 - 模式改变
        
        private Coroutine aimUICoroutine;

        private Coroutine hpUICoroutine;

        private Coroutine nrgUICoroutine;

        /// 
        /// 瞄准 UI 淡入
        /// 
        /// 持续时间
        /// 平滑时间
        private void AimUIFadeIn(float duration = 1f, float smoothTime = 0.2f)
        {
            if (aimUICoroutine != null)
                StopCoroutine(aimUICoroutine);
            aimUICoroutine = StartCoroutine(AimUI.GetComponent<CanvasGroup>().FadeToAlpha(1, duration, smoothTime));
        }
        
        /// 
        /// 瞄准 UI 淡出
        /// 
        /// 持续时间
        /// 平滑时间
        private void AimUIFadeOut(float duration = 1f, float smoothTime = 0.2f)
        {
            if (aimUICoroutine != null)
                StopCoroutine(aimUICoroutine);
            aimUICoroutine = StartCoroutine(AimUI.GetComponent<CanvasGroup>().FadeToAlpha(0, duration, smoothTime));
        }
        
        /// 
        /// 设置 UI 模式
        /// 
        /// 模式
        public void SetUIMode(TPSCharacterBehaviourMode mode)
        {
            this.mode = mode;
            switch (mode)
            {
                case TPSCharacterBehaviourMode.NoWeapon:
                    AimUIFadeOut();
                    break;
                case TPSCharacterBehaviourMode.Rifle:
                    AimUIFadeIn();
                    break;
            }
        }
    }
}

4.6 InputController v4

Assets/MeowFramework/TPSCharacter/InputSystem/TPSCharacterInputController.cs

// ----------------------------------------------
// 作者: 廉价喵
// 创建于: 14/03/2022 9:54
// 最后一次修改于: 10/04/2022 20:46
// 版权所有: CheapMeowStudio
// 描述:
// ----------------------------------------------

using System;
using System.ComponentModel;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.InputSystem;

namespace MeowFramework.TPSCharacter
{
	public class TPSCharacterInputController : SerializedMonoBehaviour
	{
		/// 
		/// 行动模式
		/// 
		[BoxGroup("Mode")]
		[ShowInInspector]
		[Sirenix.OdinInspector.ReadOnly]
		[Description("行动模式")]
		private TPSCharacterBehaviourMode mode;
		
		/// 
		/// 是否接受运动输入
		/// 
		[BoxGroup("InputValue")]
		[HorizontalGroup("InputValue/MoveInput")]
		[HorizontalGroup("InputValue/MoveInput/Left")]
		[ShowInInspector]
		[Sirenix.OdinInspector.ReadOnly]
		[Tooltip("是否接受运动输入")]
		private bool CanMoveInput = true;
			
		/// 
		/// 移动
		/// 
		[HorizontalGroup("InputValue/MoveInput/Right")]
		[LabelWidth(50)]
		[Tooltip("移动")]
		public Vector2 Move;
		
		/// 
		/// 是否接受摄像机旋转输入
		/// 
		[HorizontalGroup("InputValue/LookInput")]
		[HorizontalGroup("InputValue/LookInput/Left")]
		[ShowInInspector]
		[Sirenix.OdinInspector.ReadOnly]
		[Tooltip("是否接受摄像机旋转输入")]
		private bool CanLookInput = true;
		
		/// 
		/// 鼠标移动
		/// 
		[HorizontalGroup("InputValue/LookInput/Right")]
		[LabelWidth(50)]
		[Tooltip("鼠标移动")]
		public Vector2 Look;
		
		/// 
		/// 是否接受冲刺输入
		/// 
		[HorizontalGroup("InputValue/SprintInput")]
		[HorizontalGroup("InputValue/SprintInput/Left")]
		[ShowInInspector]
		[Sirenix.OdinInspector.ReadOnly]
		[Tooltip("是否接受冲刺输入")]
		private bool CanSprintInput = true;
		
		/// 
		/// 冲刺
		/// 
		[HorizontalGroup("InputValue/SprintInput/Right")]
		[LabelWidth(50)]
		[Tooltip("冲刺")]
		public bool Sprint;
		
		/// 
		/// 是否接受冲刺输入
		/// 
		[HorizontalGroup("InputValue/AttackInput")]
		[HorizontalGroup("InputValue/AttackInput/Left")]
		[ShowInInspector]
		[Sirenix.OdinInspector.ReadOnly]
		[Tooltip("是否接受冲刺输入")]
		private bool CanAttackInput = true;
		
		/// 
		/// 攻击
		/// 
		[HorizontalGroup("InputValue/AttackInput/Right")]
		[LabelWidth(50)]
		[Tooltip("攻击")]
		public bool Attack;
		
		/// 
		/// 是否接受瞄准输入
		/// 
		[HorizontalGroup("InputValue/AimInput")]
		[HorizontalGroup("InputValue/AimInput/Left")]
		[ShowInInspector]
		[Sirenix.OdinInspector.ReadOnly]
		[Tooltip("是否接受瞄准输入")]
		private bool CanAimInput = true;
		
		/// 
		/// 瞄准
		/// 
		[HorizontalGroup("InputValue/AimInput/Right")]
		[LabelWidth(50)]
		[Tooltip("瞄准")]
		public bool Aim;
		
		/// 
		/// 鼠标锁定
		/// 
		[BoxGroup("InputValue")]
		[Tooltip("鼠标锁定")]
		public bool CursorLocked = true;

		/// 
		/// 移动时触发的 Action
		/// 
		[HideInInspector] 
		public Action<Vector2> OnMoveAction;

		/// 
		/// 鼠标移动时,能够输入时触发的 Action
		/// 
		[HideInInspector]
		public Action<Vector2> OnLookAction;

		/// 
		/// 右键冲刺时触发的 Action
		/// 
		[HideInInspector]
		public Action OnSprintAction;

		/// 
		/// 左键攻击时触发的 Action
		/// 
		[HideInInspector]
		public Action OnAttackAction;

		/// 
		/// 右键按下与松开时触发的 Action
		/// 
		[HideInInspector]
		public Action OnAimAction;
		
		/// 
		/// 应用窗口聚焦时触发的 Action
		/// 
		[HideInInspector]
		public Action<bool> OnApplicationFocusAction;
		
		private void OnMove(InputValue value)
		{
			if (CanMoveInput)
			{
				Move = value.Get<Vector2>();
				OnMoveAction?.Invoke(Move);
			}
		}

		private void OnLook(InputValue value)
		{
			if (CanLookInput)
			{
				Look = value.Get<Vector2>();
				OnLookAction?.Invoke(Look);
			}
		}

		private void OnSprint(InputValue value)
		{
			if (CanSprintInput)
			{
				Sprint = value.isPressed;
				OnSprintAction?.Invoke();
			}
		}

		private void OnAttack(InputValue value)
		{
			if (CanAttackInput)
			{
				Attack = value.isPressed;
				OnAttackAction?.Invoke();
			}
		}

		private void OnAim(InputValue value)
		{
			if (CanAttackInput)
			{
				Aim = value.isPressed;
				OnAimAction?.Invoke();
			}
		}
		
		private void OnApplicationFocus(bool hasFocus)
		{
			SetCursorState(hasFocus);
			CursorLocked = hasFocus;
			OnApplicationFocusAction?.Invoke(CursorLocked);
		}

		private void SetCursorState(bool newState)
		{
			Cursor.lockState = newState ? CursorLockMode.Locked : CursorLockMode.None;
		}

		public void EnableMoveInput(bool shouldEnable)
		{
			CanMoveInput = shouldEnable;
			// 注意清零
			Move = Vector2.zero;
		}
		
		public void EnableLookInput(bool shouldEnable)
		{
			CanLookInput = shouldEnable;
			// 注意清零
			Look = Vector2.zero;
		}
		
		public void EnableSprintInput(bool shouldEnable)
		{
			CanSprintInput = shouldEnable;
			// 注意清零
			Sprint = false;
		}
		
		public void EnableAttackInput(bool shouldEnable)
		{
			CanAttackInput = shouldEnable;
			// 注意清零
			Attack = false;
		}
		
		public void EnableAimInput(bool shouldEnable)
		{
			CanAimInput = shouldEnable;
			// 注意清零
			Aim = false;
		}
		
		/// 
		/// 设置输入模式
		/// 
		public void SetInputMode(TPSCharacterBehaviourMode mode)
		{
			this.mode = mode;
			switch (mode)
			{
				case TPSCharacterBehaviourMode.NoWeapon:
					EnableSprintInput(true);
					break;
				case TPSCharacterBehaviourMode.Rifle:
					EnableSprintInput(false);
					break;
			}
		}
	}
	
}

代码变得多起来了
我本来想用 partial 的,但是我用了之后他识别不到我的 mono,上网搜了一下,是因为我的 partial 类的不同部分会被编译成单独的类所以我才用不了的

[Unity] 战斗系统学习 6:构建 TPS 框架 2_第7张图片

你可能感兴趣的:(游戏框架学习记录,2,unity,C#,游戏框架,FlowCanvas,TPS)