IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> [Unity] 战斗系统学习 5:基于 FlowCanvas 构建 TPS 框架 1 -> 正文阅读

[游戏开发][Unity] 战斗系统学习 5:基于 FlowCanvas 构建 TPS 框架 1

[Unity] ACT 战斗系统学习 4:重构前的第三人称控制器

以前看猴与花果山的文章,感觉大开眼界,但是没做过所以没更多体会
https://zhuanlan.zhihu.com/p/416805924
现在我才发现这真的是网上能找到的最详细的实现思路了
我犯的错误刚好就是没有想着流程,其实属性不需要流程,一个 OnChange 就行了,属性是别的流程的改变的结果,我们不是直接改变流程

说实话 FlowCanvas 学起来好麻烦
做成想要的样子也有点麻烦
为了让底层逻辑适应 FlowCanvas,还要修改底层逻辑,也好麻烦
但是毕竟策划最大……

1. Scriptable 用于角色属性

我有一段时间在纠结的就是,我到底什么时候应该用 scriptable ……因为感觉创建素材太多了很会乱
虽然直觉上他们是用在单个例子中的,需要多次引用的地方
但是我在想,对于需要动态生成的敌人,就不能要求他们都有对应的 scriptableobject 了……
这样的话,感觉框架在处理不同角色的时候行为会不一样,这容易发生一些混乱?

最初我还不知道什么东西是应该用来序列化的
然后我就列了这些

在这里插入图片描述

然后当我重构的时候我就发现,为了能够在监视器中配置 scriptableobject 我必须要把这个使用 scriptableobject 的脚本做成 mono
那么既然已经做成了 mono,那其实有些变量已经可以在监视器上序列化了,我的初衷是为了序列化才基于 scriptableobject 重构的,但是我发现我要是改成 mono 也能实现一部分的序列化
这就有点令人迷茫

不过之后我觉得我应该想懂了,像是血量体力这种被大量引用的,关系到 UI 啊,声音 啊啥的,做成 scriptableobject 很合理,像是 IsSpring 这种状态的,其实也是要影响外部的,比如我想冲刺的时候也有 UI 和 声音的变化
但是像是 timeout cost 这种,他看上去像是状态,但是又其实没那么大可能被其他部件引用的,就不用了

理所当然的,其实 GameFramework 中的 EntityData 就没有必要了,还需要自己在 Excel 中填表,填了还要生成类……
直接写成一个 ScriptableObject 就好了

在这里插入图片描述

然后接下来我在重构的时候,写的时候我就感觉关于一些状态的也是没有必要的
因为这些状态其实是一个瞬时更新的东西,无论我保存或者不保存,我打开游戏的时候都会征程更新,比如 IsGround,我打开游戏之后立即更新,那么打开游戏之前的值就无关紧要了
至于霸体这种东西,确实要存,但是这应该存在一个 Buff 列表里面而不是一个简单的变量一面
霸体是 Buff 流程的结果,而不是一个 Bool 值

2. InputController

2.1 第一版

在这里插入图片描述

在这里插入图片描述

2.2 第二版

添加了控制接受输入的功能

在这里插入图片描述

2.3 第三版

为了控制输入清零,不能直接使用布尔值控制接受输入,而是应该使用调用函数的方法

在这里插入图片描述

Assets/MeowFramework/MeowACT/InputSystem/MeowACTInputController.cs

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

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

namespace MeowFramework.MeowACT
{
	public class MeowACTInputController : SerializedMonoBehaviour
	{
		/// <summary>
		/// 是否接受运动输入
		/// </summary>
		[HorizontalGroup("MoveInput")]
		[HorizontalGroup("MoveInput/Left")]
		[ShowInInspector]
		[ReadOnly]
		[Tooltip("是否接受运动输入")]
		private bool CanMoveInput = true;
			
		/// <summary>
		/// 移动
		/// </summary>
		[HorizontalGroup("MoveInput/Right")]
		[LabelWidth(50)]
		[Tooltip("移动")]
		public Vector2 Move;
		
		/// <summary>
		/// 是否接受摄像机旋转输入
		/// </summary>
		[HorizontalGroup("LookInput")]
		[HorizontalGroup("LookInput/Left")]
		[ShowInInspector]
		[ReadOnly]
		[Tooltip("是否接受摄像机旋转输入")]
		private bool CanLookInput = true;
		
		/// <summary>
		/// 鼠标移动
		/// </summary>
		[HorizontalGroup("LookInput/Right")]
		[LabelWidth(50)]
		[Tooltip("鼠标移动")]
		public Vector2 Look;
		
		/// <summary>
		/// 是否接受冲刺输入
		/// </summary>
		[HorizontalGroup("SprintInput")]
		[HorizontalGroup("SprintInput/Left")]
		[ShowInInspector]
		[ReadOnly]
		[Tooltip("是否接受冲刺输入")]
		private bool CanSprintInput = true;
		
		/// <summary>
		/// 冲刺
		/// </summary>
		[HorizontalGroup("SprintInput/Right")]
		[LabelWidth(50)]
		[Tooltip("冲刺")]
		public bool Sprint;
		
		/// <summary>
		/// 是否接受冲刺输入
		/// </summary>
		[HorizontalGroup("AttackInput")]
		[HorizontalGroup("AttackInput/Left")]
		[ShowInInspector]
		[ReadOnly]
		[Tooltip("是否接受冲刺输入")]
		private bool CanAttackInput = true;
		
		/// <summary>
		/// 攻击
		/// </summary>
		[HorizontalGroup("AttackInput/Right")]
		[LabelWidth(50)]
		[Tooltip("攻击")]
		public bool Attack;
		
		/// <summary>
		/// 鼠标锁定
		/// </summary>
		[Tooltip("鼠标锁定")]
		public bool CursorLocked = true;

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

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

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

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

		/// <summary>
		/// 应用窗口聚焦时触发的 Action
		/// </summary>
		[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 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;
		}
	}
	
}

3. LocomotionController

3.1 第一版

在这里插入图片描述

3.2 第二版

将冲刺逻辑抽象为速度覆盖,使用速度覆盖逻辑替换冲刺+行走逻辑

在这里插入图片描述

Assets/MeowFramework/ThirdPerson/Scripts/ThirdPersonLocomotionController.cs

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

using Cinemachine;
using MeowFramework.Core;
using MeowFramework.MeowACT;
using Sirenix.OdinInspector;
using UnityEngine;

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

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

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

	    /// <summary>
	    /// 主摄像机
	    /// </summary>
	    [BoxGroup("Component")]
	    [Sirenix.OdinInspector.ReadOnly]
	    [Tooltip("主摄像机")]
	    public Camera MainCamera;
	    
	    /// <summary>
	    /// 跟随主角的摄像机
	    /// </summary>
	    [BoxGroup("Component")]
	    [Sirenix.OdinInspector.ReadOnly]
	    [Tooltip("跟随主角的摄像机")]
	    public CinemachineVirtualCamera PlayerFollowCamera;
	    
	    // 运动相关
	    
	    /// <summary>
	    /// 可资产化水平速度
	    /// </summary>
	    [BoxGroup("Velocity")]
	    [Required]
	    [Tooltip("可资产化水平速度")]
	    public ScriptableVector3Variable HorizontalVelocity;

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

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

	    /// <summary>
	    /// 水平速度方向覆盖值
	    /// </summary>
	    private Vector3 HorizontalVelocityDirectionOverride;
	    
	    // 行走相关

	    /// <summary>
	    /// 移动速度
	    /// </summary>
	    [BoxGroup("Walk")]
	    [Tooltip("移动速度")]
	    public float walkSpeed = 7f;

	    /// <summary>
	    /// 行走速度的过渡速度
	    /// </summary>
	    private Vector3 walkSmoothVelocity;
	    
	    /// <summary>
	    /// 玩家行走的过渡时间
	    /// </summary>
	    [BoxGroup("Walk")]
	    [Tooltip("玩家行走的过渡时间")]
	    public float walkSmoothTime = 0.2f;
	    
	    /// <summary>
	    /// 旋转角的过渡速度
	    /// </summary>
	    private float rotationSmoothVelocity;
	    
	    /// <summary>
	    /// 玩家旋转的过渡时间
	    /// 如果这个值过大,由于使用了 SmoothDamp,会让镜头移动出现明显的粘滞感
	    /// </summary>
	    [BoxGroup("Walk")]
	    [Tooltip("玩家旋转的过渡时间")]
	    public float rotationSmoothTime = 0.1f;

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

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

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

	    /// <summary>
	    /// 摄像机跟随点的期望俯仰角
	    /// </summary>
	    private float cinemachineTargetPitch;
	    
	    /// <summary>
	    /// 摄像机跟随点的期望偏航角
	    /// </summary>
	    private float cinemachineTargetYaw;
	    
	    /// <summary>
	    /// 摄像机跟随点的当前俯仰角和摄像机跟随点的当前偏航角组成的向量
	    /// </summary>
	    private Vector2 cinemachineCurrPY;
	    
	    /// <summary>
	    /// 摄像机跟随点的当前俯仰角的过渡速度
	    /// </summary>
	    private float cinemachinePitchSmoothVelocity;
	    
	    /// <summary>
	    /// 摄像机跟随点的当前俯仰角的过渡速度
	    /// </summary>
	    private float cinemachineYawSmoothVelocity;

	    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();
	    }
	    
	    /// <summary>
	    /// 落地检查
	    /// </summary>
	    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);
        }
	    
	    /// <summary>
	    /// 应用重力
	    /// </summary>
        private void ApplyGravity()
        {
	        if (IsGrounded && VerticalVelocity.Value < 0.0f)
		        VerticalVelocity.Value = -2f;
	        else if (VerticalVelocity.Value < terminalVelocity)
		        VerticalVelocity.Value += gravity * Time.deltaTime;
        }

        /// <summary>
        /// 移动
        /// </summary>
        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);
	        
	        // 如果有速度覆盖,则直接返回速度覆盖的结果
	        if (isHorizontalVelocityOverrided)
	        {
		        return HorizontalVelocityDirectionOverride * HorizontalVelocityOverride;
	        }
	        // 如果没有速度覆盖,则返回 Smooth 的结果
	        Vector3 targetVelocity = (ACTInput.Move == Vector2.zero) ? Vector3.zero : targetDirection * walkSpeed;
	        return Vector3.SmoothDamp(HorizontalVelocity.Value, targetVelocity, ref walkSmoothVelocity, walkSmoothTime);
        }

        /// <summary>
        /// 向移动方向旋转
        /// </summary>
        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);
	        }
        }

        /// <summary>
        /// 摄像机旋转
        /// </summary>
        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);
        }

        /// <summary>
        /// 基于摄像机方向,向键盘输入方向移动的方向
        /// </summary>
        /// <returns></returns>
        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;
        }
        
        /// <summary>
        /// 取消速度覆盖
        /// </summary>
        public void CancelHorizontalVelocityOverride()
        {
	        isHorizontalVelocityOverrided = false;
        }
        
        /// <summary>
        /// 默认速度覆盖方向为当前摄像机方向+键盘输入方向
        /// </summary>
        /// <param name="speed">速度</param>
        public void SetHorizontalVelocityOverride(float speed)
        {
	        isHorizontalVelocityOverrided = true;
	        HorizontalVelocityOverride = speed;
	        HorizontalVelocityDirectionOverride = GetInputDirectionBaseOnCamera();
        }
        
        /// <summary>
        /// 允许设置速度覆盖方向
        /// </summary>
        /// <param name="speed">速度</param>
        /// <param name="dir">速度方向</param>
        public void SetHorizontalVelocityOverride(float speed, Vector3 dir)
        {
	        isHorizontalVelocityOverrided = true;
	        HorizontalVelocityOverride = speed;
	        HorizontalVelocityDirectionOverride = dir;
        }
    }
}

4. AnimationController

4.1 第一版

最初是功能键很少的

在这里插入图片描述

Assets/MeowFramework/ThirdPerson/Scripts/ThirdPersonAnimationController.cs

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

using System;
using Sirenix.OdinInspector;
using UnityEngine;

namespace MeowFramework
{
    /// <summary>
    /// 第三人称动画状态机控制器
    /// </summary>
    public class ThirdPersonAnimationController : SerializedMonoBehaviour
    {
        // 组件

        /// <summary>
        /// 第三人称运动管理器
        /// </summary>
        [BoxGroup("Component")]
        [Required]
        [Tooltip("第三人称运动管理器")]
        public ThirdPersonLocomotionController LocomotionController;
        
        /// <summary>
        /// 动画控制器
        /// </summary>
        [BoxGroup("Component")]
        [Required]
        [Tooltip("动画控制器")]
        public Animator Anim;
        
        // id
	    
        /// <summary>
        /// 动画状态机的速度参数的 id
        /// </summary>
        private int animIDSpeed;
        
        /// <summary>
        /// 动画状态机的落地参数的 id
        /// </summary>
        private int animIDGrounded;
        
        /// <summary>
        /// 动画状态机的自由落体参数的 id
        /// </summary>
        private int animIDFreeFall;
        
        /// <summary>
        /// 动画状态机的近战攻击参数的 id
        /// </summary>
        private int animIDMeleeAttack;
        
        // 混合值
        
        /// <summary>
        /// 跑步动画混合值
        /// </summary>
        private float moveAnimBlend;
        
        /// <summary>
        /// 跑步动画混合值的过渡速度
        /// </summary>
        private float moveAnimBlendSmoothVelocity;
        
        /// <summary>
        /// 跑步动画混合值的过渡时间
        /// </summary>
        [BoxGroup("Move")]
        [Tooltip("跑步动画混合值的过渡时间")]
        public float moveAnimBlendSmoothTime = 0.2f;

        public void Start()
        {
            AssignAnimationIDs();
        }

        public void Update()
        {
            SetAnimatorValue();
        }

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

        /// <summary>
        /// 设置动画状态机参数
        /// </summary>
        public void SetAnimatorValue()
        {
            // 着地
            Anim.SetBool(animIDGrounded, LocomotionController.IsGrounded);
            
            // 跳跃
            if (LocomotionController.IsGrounded)
                Anim.SetBool(animIDFreeFall, false);
            // 自由下落
            else
                Anim.SetBool(animIDFreeFall, true);
            
            // 移动
            moveAnimBlend = Mathf.SmoothDamp(moveAnimBlend, LocomotionController.HorizontalVelocity.Value.magnitude, ref moveAnimBlendSmoothVelocity, moveAnimBlendSmoothTime);
            Anim.SetFloat(animIDSpeed, moveAnimBlend);
        }

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

5. EventNode

5.1 第一版

我本来希望直接拿到 Action,对一个泛型 Action 操作
但是后面发现我这样拿到的是一个值类型,我还拿不到引用类型

在这里插入图片描述

5.2 第二版

没有办法的话那就只能一个事件做一个节点了

在这里插入图片描述

5.3 第三版

把 InpurController 做成 BBParameter 还是更好看……

在这里插入图片描述

6. 冲刺

6.1 第一版

直接在 ThirdPersonLocomotionController 中,进行移动前,判断是否冲刺,是冲刺则进入冲刺速度计算公式

6.2 第二版

在 ThirdPersonLocomotionController 中将冲刺速度判断逻辑改造为速度覆盖逻辑,进行移动前,判断是否有速度覆盖,有速度覆盖则进入速度覆盖计算公式

在这里插入图片描述

6.3 第三版

第二版这个速度覆盖的逻辑是稳定的,不管怎么覆盖都是这样
唯一可能改变的就是前面控制时间的部分
因此我就把后面的部分封装起来

在这里插入图片描述

封装成的节点的代码

Assets/MeowFramework/ParadoxNotionExtension/Ability/OverrideHorizontalVelocity.cs

// ----------------------------------------------
// 作者: 廉价喵
// 创建于: 07/04/2022 5:45
// 最后一次修改于: 07/04/2022 6:08
// 版权所有: CheapMeowStudio
// 描述:
// ----------------------------------------------

using System.Collections;
using MeowFramework;
using MeowFramework.MeowACT;
using ParadoxNotion.Design;
using UnityEngine;

namespace FlowCanvas.Nodes
{
    [Category("MeowFramework/Ability")]
    [Description("在一段时间内覆盖水平速度")]
    public class OverrideHorizontalVelocity : LatentActionNode<MeowACTInputController, ThirdPersonLocomotionController, float, float, float>
    {
        public float timeLeft { get; private set; }
        public float normalized { get; private set; }
        
        public override IEnumerator Invoke(MeowACTInputController inputController, ThirdPersonLocomotionController locomotionController, float velocityOverride = 0f, float time = 1f, float timeScale = 1f)
        {
            // 空值检查
            if (inputController == null)
                yield return null;
            if (locomotionController == null)
                yield return null;
            
            inputController.EnableMoveInput(false);
            locomotionController.SetHorizontalVelocityOverride(velocityOverride);
            
            timeLeft = time;
            while ( timeLeft > 0 ) {
                timeLeft -= Time.deltaTime * timeScale;
                timeLeft = Mathf.Max(timeLeft, 0);
                normalized = timeLeft / time;
                yield return null;
            }
            
            locomotionController.CancelHorizontalVelocityOverride();
            inputController.EnableMoveInput(true);
        }
    }
}

6.4 第四版

组件的连线还是用黑板值比较好,连线太多会很难看
然后要中断协程应该用 yield break

在这里插入图片描述

6.5 第五版

想想还是把 TimeScale 做成 BBParameter 把

在这里插入图片描述

事件节点也改了参数之后

在这里插入图片描述

节点代码:

// ----------------------------------------------
// 作者: 廉价喵
// 创建于: 07/04/2022 5:45
// 最后一次修改于: 07/04/2022 10:41
// 版权所有: CheapMeowStudio
// 描述:
// ----------------------------------------------

using System.Collections;
using MeowFramework;
using MeowFramework.MeowACT;
using NodeCanvas.Framework;
using ParadoxNotion.Design;
using UnityEngine;

namespace FlowCanvas.Nodes
{
    [Category("MeowFramework/Ability")]
    [Description("在一段时间内覆盖水平速度")]
    public class OverrideHorizontalVelocity : LatentActionNode<float, float, float>
    {
        /// <summary>
        /// 输入控制器
        /// </summary>
        public BBParameter<MeowACTInputController> InputController;

        /// <summary>
        /// 移动控制器
        /// </summary>
        public BBParameter<ThirdPersonLocomotionController> LocomotionController;
        
        public float timeLeft { get; private set; }
        public float normalized { get; private set; }
        
        /// <summary>
        /// 速度覆盖的协程函数
        /// </summary>
        /// <param name="velocityOverride">速度覆盖值</param>
        /// <param name="time">时长</param>
        /// <param name="timeScale">时间尺度</param>
        /// <returns></returns>
        public override IEnumerator Invoke(float velocityOverride = 0f, float time = 1f, float timeScale = 1f)
        {
            // 空值检查
            if (InputController.value == null)
                yield return null;
            if (LocomotionController.value == null)
                yield return null;
            
            InputController.value.EnableMoveInput(false);
            LocomotionController.value.SetHorizontalVelocityOverride(velocityOverride);
            
            timeLeft = time;
            while ( timeLeft > 0 ) {
                timeLeft -= Time.deltaTime * timeScale;
                timeLeft = Mathf.Max(timeLeft, 0);
                normalized = timeLeft / time;
                yield return null;
            }
            
            LocomotionController.value.CancelHorizontalVelocityOverride();
            InputController.value.EnableMoveInput(true);
            
            // 最后要返回 null,不然协程函数末尾的语句的执行时间会有问题
            yield return null;
        }
    }
}

7. 瞄准

7.1 第一版

最初是想用类似 Wait Until 的思路做

在这里插入图片描述

节点代码:

Assets/MeowFramework/ParadoxNotionExtension/Ability/Aiming.cs

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

using System.Collections;
using Cinemachine;
using MeowFramework;
using MeowFramework.MeowACT;
using NodeCanvas.Framework;
using ParadoxNotion.Design;
using UnityEngine;

namespace FlowCanvas.Nodes
{
    [Category("MeowFramework/Ability")]
    [Description("进入瞄准状态")]
    public class Aiming : LatentActionNode
    {
        /// <summary>
        /// 输入控制器
        /// </summary>
        [Description("输入控制器")]
        public BBParameter<MeowACTInputController> InputController;

        /// <summary>
        /// 移动控制器
        /// </summary>
        [Description("移动控制器")]
        public BBParameter<ThirdPersonLocomotionController> LocomotionController;

        /// <summary>
        /// 玩家摄像机
        /// </summary>
        [Description("玩家摄像机")]
        public BBParameter<CinemachineVirtualCamera> playerCamera;
        
        /// <summary>
        /// 时间比尺
        /// </summary>
        [Description("时间比尺")]
        public BBParameter<float> TimeScale = 1f;

        /// <summary>
        /// 结束条件
        /// </summary>
        private ValueInput<bool> condition;

        /// <summary>
        /// 摄像机移动的过渡时间
        /// </summary>
        [Description("摄像机移动的过渡时间")]
        public BBParameter<float> CameraMoveTransitionTime = 1f;
        
        /// <summary>
        /// 待机状态下摄像机的侧向位置
        /// </summary>
        [Description("待机状态下摄像机的侧向位置")]
        public BBParameter<float> IdleSide = 0.5f;
        
        /// <summary>
        /// 瞄准状态下摄像机的侧向位置
        /// </summary>
        [Description("瞄准状态下摄像机的侧向位置")]
        public BBParameter<float> AimingSide = 1f;
        
        /// <summary>
        /// 摄像机侧向位置的平滑速度
        /// </summary>
        private float cameraSideSmoothVelocity;
        
        /// <summary>
        /// 摄像机侧向位置的平滑时间
        /// </summary>
        [Description("摄像机侧向位置的平滑时间")]
        public BBParameter<float> CameraSideSmoothTime = 0.2f;
        
        /// <summary>
        /// 剩余时间
        /// </summary>
        private float timeLeft { get; set; }

        public override IEnumerator Invoke()
        {
            yield return CameraMove(AimingSide.value);
            yield return new UnityEngine.WaitUntil(condition.GetValue);
            yield return CameraMove(IdleSide.value);
        }

        private IEnumerator CameraMove(float newSide)
        {
            // 摄像机第三人称跟随组件
            var camera3rdPersonFollow =
                playerCamera.value.GetCinemachineComponent<Cinemachine3rdPersonFollow>();
            
            // 初始化计时器
            timeLeft = CameraMoveTransitionTime.value;
            
            // 在给定时间内平滑
            // 平滑时间结束时,被平滑项接近终点值但不是终点值
            // 因此最后需要给被平滑项赋终点值,这可能产生一个抖动
            // 因此平滑时间需要在保证效果的同时尽可能小,才能让最后的抖动变小
            while ( timeLeft > 0 ) {
                timeLeft -= Time.deltaTime * TimeScale.value;
                camera3rdPersonFollow.CameraSide = Mathf.SmoothDamp(camera3rdPersonFollow.CameraSide, newSide,
                    ref cameraSideSmoothVelocity, CameraSideSmoothTime.value);
                yield return null;
            }
            
            // 摄像机侧向位置赋终点值
            camera3rdPersonFollow.CameraSide = newSide;
            
            yield return null;
        }
        
        //since we want to check the condition per frame, this is implementing this way instead of a parameter
        protected override void OnRegisterExtraPorts(FlowNode node)
        {
            condition = node.AddValueInput<bool>("Condition");
        }
    }
}

但是这样有个问题就是第一个协程启动了,需要等到它执行完才能执行
因此如果你在第一个协程启动的时候满足了退出条件,它还要等到第一个协程执行完才能退出
想要退出时再立即进入也同理办不到
因此是不能用协程控制摄像机移动的

7.2 第二版

之前想着用一个协程解决进入和退出
想想就感觉有点乱
之后把过程分解到只有一个移动
进入和退出是通过打断正在进行的移动实现的
这就很方便了

在这里插入图片描述

7.3 第三版

添加了 FOV 变化

在这里插入图片描述

添加速度改变

在这里插入图片描述

节点代码:

Assets/MeowFramework/ParadoxNotionExtension/Ability/PlayerCameraSmoothMove.cs

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

using System.Collections;
using Cinemachine;
using MeowFramework;
using MeowFramework.MeowACT;
using NodeCanvas.Framework;
using ParadoxNotion.Design;
using UnityEngine;

namespace FlowCanvas.Nodes
{
    [Category("MeowFramework/Ability")]
    [Description("玩家摄像机平滑移动")]
    public class PlayerCameraSmoothMove : LatentActionNode
    {
        /// <summary>
        /// 输入控制器
        /// </summary>
        [Description("输入控制器")]
        public BBParameter<MeowACTInputController> InputController;

        /// <summary>
        /// 移动控制器
        /// </summary>
        [Description("移动控制器")]
        public BBParameter<ThirdPersonLocomotionController> LocomotionController;

        /// <summary>
        /// 玩家摄像机
        /// </summary>
        [Description("玩家摄像机")]
        public BBParameter<CinemachineVirtualCamera> playerCamera;
        
        /// <summary>
        /// 时间比尺
        /// </summary>
        [Description("时间比尺")]
        public BBParameter<float> TimeScale = 1f;

        /// <summary>
        /// 摄像机移动的过渡时间
        /// </summary>
        [Description("摄像机移动的过渡时间")]
        public BBParameter<float> CameraMoveTransitionTime = 1f;

        /// <summary>
        /// 摄像机的目标 FOV
        /// </summary>
        [Description("摄像机的目标 FOV")]
        public BBParameter<float> TargetFOV = 30f;
        
        /// <summary>
        /// 摄像机的目标 FOV 平滑速度
        /// </summary>
        private float FOVSmoothVelocity;
        
        /// <summary>
        /// 摄像机的目标 FOV 的平滑时间
        /// </summary>
        [Description("摄像机的目标 FOV 的平滑时间")]
        public BBParameter<float> FOVSmoothTime = 0.2f;
        
        /// <summary>
        /// 摄像机的目标侧向位置
        /// </summary>
        [Description("摄像机的目标侧向位置")]
        public BBParameter<float> TargetSide = 0.5f;

        /// <summary>
        /// 摄像机侧向位置的平滑速度
        /// </summary>
        private float cameraSideSmoothVelocity;
        
        /// <summary>
        /// 摄像机侧向位置的平滑时间
        /// </summary>
        [Description("摄像机侧向位置的平滑时间")]
        public BBParameter<float> CameraSideSmoothTime = 0.2f;
        
        /// <summary>
        /// 剩余时间
        /// </summary>
        private float timeLeft { get; set; }

        public override IEnumerator Invoke()
        {
            // 摄像机第三人称跟随组件
            var camera3rdPersonFollow =
                playerCamera.value.GetCinemachineComponent<Cinemachine3rdPersonFollow>();
            
            // 初始化计时器
            timeLeft = CameraMoveTransitionTime.value;
            
            // 在给定时间内平滑
            // 平滑时间结束时,被平滑项接近终点值但不是终点值
            // 因此最后需要给被平滑项赋终点值,这可能产生一个抖动
            // 因此平滑时间需要在保证效果的同时尽可能小,才能让最后的抖动变小
            while ( timeLeft > 0 ) {
                timeLeft -= Time.deltaTime * TimeScale.value;
                playerCamera.value.m_Lens.FieldOfView = Mathf.SmoothDamp(playerCamera.value.m_Lens.FieldOfView, TargetFOV.value,
                    ref FOVSmoothVelocity, FOVSmoothTime.value);
                camera3rdPersonFollow.CameraSide = Mathf.SmoothDamp(camera3rdPersonFollow.CameraSide, TargetSide.value,
                    ref cameraSideSmoothVelocity, CameraSideSmoothTime.value);
                yield return null;
            }

            // 摄像机焦距设置赋终点值
            playerCamera.value.m_Lens.FieldOfView = TargetFOV.value;
            // 摄像机侧向位置赋终点值
            camera3rdPersonFollow.CameraSide = TargetSide.value;
            
            yield return null;
        }
    }
}
  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2022-04-09 18:49:29  更:2022-04-09 18:51:20 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/16 21:07:25-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码