[Unity] 战斗系统学习 6:构建 TPS 框架 2
1. Port 与 BBParameter
我一开始就是公开组件接口,那个时候是因为我还只会用 AddValueInput,不会用 BBParameter
但是后来我发现 BBParameter 很简单之后,我就想在每个地方都用上 BBParameter 了,因为我希望连线尽可能少
但是后面我又发现其实直接用接口的话,他会设置默认的 self 传进来,这样其实对于有默认值的接口,也不用连很多线
那么把这个接口暴露出来的意义就是,让人知道明确知道有这么一个东西需要连,一眼看过去就知道,而不需要点开这个节点看需要什么黑板值
而黑板值用在一些具体细节的布置上,比如我摄像机的偏移为多少,角色速度调到多少,很多时候这些都是一个死的东西,不需要连线,也不需要黑板上有值,给个字面值就可以了的这种
但是由于 FlowCanvas 只会对多 Component 接口的第一个接口设置默认的 self,这就强迫设计者一个函数只能用一个组件
某种程度上也算是强迫解耦了?
1.1 冲刺 v6
例如原来我会把禁止玩家输入和速度覆盖放在一起,然后用两个组件,然后这两个组件都是黑板值,现在我把组件接口暴露出来,然后解耦了禁止输入和速度覆盖
2. 自定义节点与公有函数
一开始我想什么都用自定义节点
但是我发现了单一组件原则之后,对比自定义节点与公有函数,它们都是处理一个组件内的事情,那为什么不直接在组件里面写函数呢?
组件内写函数反而方便在 FlowScript 中组合
只有对于那些真的很基础的节点才需要自定义节点,比如我想要给 wait 添加一个时间缩放啥的,不得不新建一个带缩放因子变量的节点
2.1 冲刺 v7
比如我原来会把速度覆盖与计时放在一起,现在想想确实没必要,新建一个这样的节点浪费工作量
3. LatentActionNode 的 Break
LatentActionNode 的 Break 会直接触发 Finish
3.1 瞄准 v4
我原来是想在协程结束之后把瞄准图标 SetActive(false)
的
但是 Break
会直接触发协程的 Finish
,Finish
又会触发 SetActive(false)
,因此这就相当于我要显示图标的同时把他 SetActive(true)
SetActive(false)
,这就很迷惑
所以干脆不要这个 SetActive
了,直接一直为 true
把
4. 运动模式
4.1 瞄准 v5
后面我又觉得其实没有必要特意把一个 CanvasGroup 连出来
于是瞄准变成这样
在这里我使用了第二个 set style 然后我还注意到所有的功能都是迫不得已一个组件的
然后接下来我又发现 locomotion 的计算也需要改变,因为我的基本运动是人物会朝着摄像机方向+输入方向的,持枪的时候人物应该始终朝着前面,所以人物移动应该是朝着摄像机的前方向和右方向的
这就要求我再在一个地方注意更改
这样我就想到,为什么我不给一个组件做一个模式改变的函数呢?模式改变所需要的参数,模式改变具体要用的函数,感觉也不需要在运行时用到,暴露出来也没有意义
4.2 瞄准 v6
于是修改之后就变成这样
看上去就很简洁
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
{/// <summary>/// 第三人称运动控制器/// </summary>public class TPSCharacterLocomotionController : 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 TPSCharacterInputController 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("Mode")][ShowInInspector][Sirenix.OdinInspector.ReadOnly][Description("行动模式")]private TPSCharacterBehaviourMode mode;/// <summary>/// 切换模式的过渡时间/// </summary>[BoxGroup("Mode")][ShowInInspector][Description("切换模式的过渡时间")]private float modeTransitionTime = 1f;/// <summary>/// 没有武器时摄像机的 FOV/// </summary>[BoxGroup("Mode")][ShowInInspector][Description("摄像机的目标 FOV")]private float noWeaponFOV = 40f;/// <summary>/// 持步枪时摄像机的 FOV/// </summary>[BoxGroup("Mode")][ShowInInspector][Description("摄像机的目标 FOV")]private float rifleFOV = 30f;/// <summary>/// 摄像机的 FOV 的平滑时间/// </summary>[BoxGroup("Mode")][ShowInInspector][Description("摄像机的目标 FOV 的平滑时间")]private float fovSmoothTime = 0.2f;/// <summary>/// 没有武器时摄像机的侧向位置/// </summary>[BoxGroup("Mode")][ShowInInspector][Description("摄像机的目标侧向位置")]private float noWeaponSide = 0.5f;/// <summary>/// 持步枪时摄像机的侧向位置/// </summary>[BoxGroup("Mode")][ShowInInspector][Description("摄像机的目标侧向位置")]private float rifleSide = 1f;/// <summary>/// 摄像机侧向位置的平滑时间/// </summary>[BoxGroup("Mode")][ShowInInspector][Description("摄像机侧向位置的平滑时间")]private float cameraSideSmoothTime = 0.2f;// 运动相关/// <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>[BoxGroup("Walk")][ShowInInspector][Tooltip("移动速度")]private float walkSpeed = 7f;/// <summary>/// 玩家行走的过渡时间/// </summary>[BoxGroup("Walk")][ShowInInspector][Tooltip("玩家行走的过渡时间")]private float walkSmoothTime = 0.2f;/// <summary>/// 玩家旋转的过渡时间/// 如果这个值过大,由于使用了 SmoothDamp,会让镜头移动出现明显的粘滞感/// </summary>[BoxGroup("Walk")][ShowInInspector][Tooltip("玩家旋转的过渡时间")]private float rotationSmoothTime = 0.1f;// 物理相关/// <summary>/// 重力系数/// </summary>[BoxGroup("Gravity")][ShowInInspector][Tooltip("重力系数")]private float gravity = -9.8f;/// <summary>/// 最大下落速度/// </summary>[BoxGroup("Gravity")][ShowInInspector][Tooltip("最大下落速度")]private float terminalVelocity = 53f;// 落地相关/// <summary>/// 是否落地/// </summary>[BoxGroup("GroundCheck")][ShowInInspector][Sirenix.OdinInspector.ReadOnly][Tooltip("是否落地")]private bool isGrounded;/// <summary>/// 是否落地/// </summary>public bool IsGrounded => isGrounded;/// <summary>/// 落地球形碰撞检测中心点的竖向偏移量/// </summary>[BoxGroup("GroundCheck")][ShowInInspector][Tooltip("落地球形碰撞检测中心点的竖向偏移量")]private float groundedOffset = -0.14f;/// <summary>/// 落地球形碰撞检测的半径/// </summary>[BoxGroup("GroundCheck")][ShowInInspector][Tooltip("落地球形碰撞检测的半径")]private float groundedRadius = 0.28f;/// <summary>/// 落地球形碰撞检测的层级/// </summary>[BoxGroup("GroundCheck")][ShowInInspector][Tooltip("落地球形碰撞检测的层级")]private int groundLayers = 1;// 摄像机相关/// <summary>/// 摄像机是否固定/// </summary>[BoxGroup("Camera")][ShowInInspector][Sirenix.OdinInspector.ReadOnly][Tooltip("摄像机是否固定")]private bool IsCameraFixed = false;/// <summary>/// 摄像机俯仰角覆盖值/// </summary>[BoxGroup("Camera")][ShowInInspector][Tooltip("摄像机俯仰角覆盖值")]private float cameraPitchOverride = 0f;/// <summary>/// 摄像机转动速度/// </summary>[BoxGroup("Camera")][ShowInInspector][Tooltip("摄像机转动速度")]private float cameraRotSpeed = 25f;/// <summary>/// 摄像机最大俯仰角/// </summary>[BoxGroup("Camera")][PropertyRange(0,90)][ShowInInspector][Tooltip("摄像机最大俯仰角")]private float topClamp = 70f; /// <summary>/// 摄像机最小俯仰角/// </summary>[BoxGroup("Camera")][PropertyRange(-90,0)][ShowInInspector][Tooltip("摄像机最小俯仰角")]private float bottomClamp = -30f;/// <summary>/// 摄像机跟随点的当前俯仰角的过渡时间/// </summary>[BoxGroup("Camera")][ShowInInspector][Tooltip("摄像机跟随点的当前俯仰角的过渡时间")]private float cinemachinePitchSmoothTime = 0.1f;/// <summary>/// 摄像机跟随点的当前偏航角的过渡时间/// </summary>[BoxGroup("Camera")][ShowInInspector][Tooltip("摄像机跟随点的当前偏航角的过渡时间")]private float cinemachineYawSmoothTime = 0.1f;// 缓存// 缓存 - 速度覆盖/// <summary>/// 水平速度方向覆盖值/// </summary>private Vector3 horizontalVelocityDirectionOverride;// 缓存 - 玩家行走/// <summary>/// 行走速度的过渡速度/// </summary>private Vector3 walkSmoothVelocity;/// <summary>/// 旋转角的过渡速度/// </summary>private float rotationSmoothVelocity;// 缓存 - 摄像机旋转/// <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;// 缓存 - 运动模式/// <summary>/// 模式改变协程/// </summary>private Coroutine modeChangeCoroutine;/// <summary>/// 摄像机的目标 FOV 平滑速度/// </summary>private float fovSmoothVelocity;/// <summary>/// 摄像机侧向位置的平滑速度/// </summary>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();}/// <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);// 如果有速度覆盖,则直接返回速度覆盖的结果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);}/// <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 fixedif (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 degreescinemachineTargetPitch = 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 targetCMCameraFollowTarget.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 不会抖动,因此采用 cinemachineTargetYawfloat 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;}// 后面这个可能会有强制初始方向和强制始终一个方向// 现在是懒得做了// /// <summary>// /// 允许设置速度覆盖方向// /// </summary>// /// <param name="speed">速度</param>// /// <param name="dir">速度方向</param>// public void SetHorizontalVelocityOverride(float speed, Vector3 dir)// {// isHorizontalVelocityOverrided = true;// HorizontalVelocityOverride = speed;// HorizontalVelocityDirectionOverride = dir;// }/// <summary>/// 开始模式改变的协程函数/// </summary>/// <param name="targetFOV">目标 FOV</param>/// <param name="targetSide">目标侧向位置</param>/// <returns></returns>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;}/// <summary>/// 改变运动模式/// </summary>/// <param name="mode">模式</param>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
{/// <summary>/// 第三人称动画状态机控制器/// </summary>public class TPSCharacterAnimationController : SerializedMonoBehaviour{// 组件/// <summary>/// 第三人称运动管理器/// </summary>[BoxGroup("Component")][Required][Tooltip("第三人称运动管理器")]public TPSCharacterLocomotionController LocomotionController;/// <summary>/// 动画控制器/// </summary>[BoxGroup("Component")][Required][Tooltip("动画控制器")]public Animator Anim;/// <summary>/// 行动模式/// </summary>[BoxGroup("Mode")][ShowInInspector][Sirenix.OdinInspector.ReadOnly][Description("行动模式")]private TPSCharacterBehaviourMode mode;/// <summary>/// 切换模式的过渡时间/// </summary>[BoxGroup("Mode")][ShowInInspector][Description("切换模式的过渡时间")]private float modeTransitionTime = 1f;/// <summary>/// 层级平滑时间/// </summary>[BoxGroup("Mode")][ShowInInspector][Description("层级平滑时间")]private float layerWeightSmoothTime = 0.2f;/// <summary>/// 向前的速度/// </summary>[BoxGroup("AnimatorParameter")][Sirenix.OdinInspector.ReadOnly][ShowInInspector][Description("向前的速度")]private float forwardSpeed;/// <summary>/// 向右的速度/// </summary>[BoxGroup("AnimatorParameter")][Sirenix.OdinInspector.ReadOnly][ShowInInspector][Description("向右的速度")]private float rightSpeed;// 缓存// 缓存 - 动画机参数计算/// <summary>/// 摄像机的前方/// </summary>private Vector3 cameraForward;/// <summary>/// 摄像机的右方/// </summary>private Vector3 cameraRight;// 缓存 - id/// <summary>/// 动画状态机的摄像机前方速度参数的 id/// </summary>private int animIDForwardSpeed;/// <summary>/// 动画状态机的摄像机右方速度参数的 id/// </summary>private int animIDRightSpeed;/// <summary>/// 动画状态机的落地参数的 id/// </summary>private int animIDGrounded;/// <summary>/// 动画状态机的自由落体参数的 id/// </summary>private int animIDFreeFall;/// <summary>/// 动画状态机的近战攻击参数的 id/// </summary>private int animIDMeleeAttack;// 缓存 - 模式改变/// <summary>/// 模式改变协程/// </summary>private Coroutine modeChangeCoroutine;/// <summary>/// 旧层级权重平滑速度/// </summary>private float fromLayerWeightSmoothVelocity;/// <summary>/// 新层级权重平滑速度/// </summary>private float toLayerWeightSmoothVelocity;public void Start(){AssignAnimationIDs();}public void Update(){SetAnimatorValue();}/// <summary>/// 初始化动画状态机参数/// </summary>public void AssignAnimationIDs(){animIDForwardSpeed = Animator.StringToHash("ForwardSpeed");animIDRightSpeed = Animator.StringToHash("RightSpeed");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);// 自由下落elseAnim.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);}/// <summary>/// 开始模式改变的协程函数/// </summary>/// <param name="targetFOV">目标 FOV</param>/// <param name="targetSide">目标侧向位置</param>/// <returns></returns>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;}/// <summary>/// 设置动画模式/// </summary>/// <param name="mode">模式</param>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
{/// <summary>/// 第三人称 UI 控制器/// </summary>public class TPSCharacterUIController : SerializedMonoBehaviour{// 组件/// <summary>/// 瞄准 UI/// </summary>[Required][BoxGroup("Component")][Description("瞄准 UI")]public GameObject AimUI;/// <summary>/// 血量 UI/// </summary>[Required][BoxGroup("Component")][Description("血量 UI")]public GameObject HPUI;/// <summary>/// 体力条 UI/// </summary>[Required][BoxGroup("Component")][Description("体力条 UI")]public GameObject NRGUI;// 模式/// <summary>/// 行动模式/// </summary>[BoxGroup("Mode")][ShowInInspector][Sirenix.OdinInspector.ReadOnly][Description("行动模式")]private TPSCharacterBehaviourMode mode;// 缓存// 缓存 - 模式改变private Coroutine aimUICoroutine;private Coroutine hpUICoroutine;private Coroutine nrgUICoroutine;/// <summary>/// 瞄准 UI 淡入/// </summary>/// <param name="duration">持续时间</param>/// <param name="smoothTime">平滑时间</param>private void AimUIFadeIn(float duration = 1f, float smoothTime = 0.2f){if (aimUICoroutine != null)StopCoroutine(aimUICoroutine);aimUICoroutine = StartCoroutine(AimUI.GetComponent<CanvasGroup>().FadeToAlpha(1, duration, smoothTime));}/// <summary>/// 瞄准 UI 淡出/// </summary>/// <param name="duration">持续时间</param>/// <param name="smoothTime">平滑时间</param>private void AimUIFadeOut(float duration = 1f, float smoothTime = 0.2f){if (aimUICoroutine != null)StopCoroutine(aimUICoroutine);aimUICoroutine = StartCoroutine(AimUI.GetComponent<CanvasGroup>().FadeToAlpha(0, duration, smoothTime));}/// <summary>/// 设置 UI 模式/// </summary>/// <param name="mode">模式</param>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{/// <summary>/// 行动模式/// </summary>[BoxGroup("Mode")][ShowInInspector][Sirenix.OdinInspector.ReadOnly][Description("行动模式")]private TPSCharacterBehaviourMode mode;/// <summary>/// 是否接受运动输入/// </summary>[BoxGroup("InputValue")][HorizontalGroup("InputValue/MoveInput")][HorizontalGroup("InputValue/MoveInput/Left")][ShowInInspector][Sirenix.OdinInspector.ReadOnly][Tooltip("是否接受运动输入")]private bool CanMoveInput = true;/// <summary>/// 移动/// </summary>[HorizontalGroup("InputValue/MoveInput/Right")][LabelWidth(50)][Tooltip("移动")]public Vector2 Move;/// <summary>/// 是否接受摄像机旋转输入/// </summary>[HorizontalGroup("InputValue/LookInput")][HorizontalGroup("InputValue/LookInput/Left")][ShowInInspector][Sirenix.OdinInspector.ReadOnly][Tooltip("是否接受摄像机旋转输入")]private bool CanLookInput = true;/// <summary>/// 鼠标移动/// </summary>[HorizontalGroup("InputValue/LookInput/Right")][LabelWidth(50)][Tooltip("鼠标移动")]public Vector2 Look;/// <summary>/// 是否接受冲刺输入/// </summary>[HorizontalGroup("InputValue/SprintInput")][HorizontalGroup("InputValue/SprintInput/Left")][ShowInInspector][Sirenix.OdinInspector.ReadOnly][Tooltip("是否接受冲刺输入")]private bool CanSprintInput = true;/// <summary>/// 冲刺/// </summary>[HorizontalGroup("InputValue/SprintInput/Right")][LabelWidth(50)][Tooltip("冲刺")]public bool Sprint;/// <summary>/// 是否接受冲刺输入/// </summary>[HorizontalGroup("InputValue/AttackInput")][HorizontalGroup("InputValue/AttackInput/Left")][ShowInInspector][Sirenix.OdinInspector.ReadOnly][Tooltip("是否接受冲刺输入")]private bool CanAttackInput = true;/// <summary>/// 攻击/// </summary>[HorizontalGroup("InputValue/AttackInput/Right")][LabelWidth(50)][Tooltip("攻击")]public bool Attack;/// <summary>/// 是否接受瞄准输入/// </summary>[HorizontalGroup("InputValue/AimInput")][HorizontalGroup("InputValue/AimInput/Left")][ShowInInspector][Sirenix.OdinInspector.ReadOnly][Tooltip("是否接受瞄准输入")]private bool CanAimInput = true;/// <summary>/// 瞄准/// </summary>[HorizontalGroup("InputValue/AimInput/Right")][LabelWidth(50)][Tooltip("瞄准")]public bool Aim;/// <summary>/// 鼠标锁定/// </summary>[BoxGroup("InputValue")][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 OnAimAction;/// <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 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;}/// <summary>/// 设置输入模式/// </summary>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相关推荐
- [Unity] 战斗系统学习 11:Buff 框架 1
1. Time 一开始我是希望有一个 ScriptableObject 可以用来计时的 对于一些需要持续计时的事情可以用 1.1 ScriptableTimer Assets/MeowFramewor ...
- [Unity] 战斗系统学习 4:FlowCanvas 中的 LatentActionNode
[Unity] 战斗系统学习 2:FlowCanvas 中的 SubGraph 1. 并行执行 FlowScript 可能的魔改方向 其实我原本是想魔改 FlowCanvas 使其支持并行 FlowS ...
- [Unity] 战斗系统学习 5:构建 TPS 框架 1
[Unity] ACT 战斗系统学习 4:重构前的第三人称控制器 以前看猴与花果山的文章,感觉大开眼界,但是没做过所以没更多体会 https://zhuanlan.zhihu.com/p/416805 ...
- [Unity] 战斗系统学习 8:构建 TPS 框架 3:mono 组件
1. 框架组件 1.1 FrameworkComponent v1 一开始我想的是这样做框架组件嘛,跟 GF 学的 但是后来我才知道 static 变量是默认在监视器上不显示的,怪不得 GF 不在 A ...
- [Unity] 战斗系统学习 9:构建 TPS 框架 4
1. 技能框架 我的需求是要在 FlowScript 中写技能,然后在角色中引用这个 FlowScript,当需要释放技能的时候就启用这个 FlowScript 1.1 ScriptableAbili ...
- [Unity] 战斗系统学习 13:Switchable 2
1. Switchable v2 改了这两个 TPSCharacterAnimationController.Mode TPSCharacterLocomotionController.Mode 之后 ...
- [Unity] 战斗系统学习 10:ActorAttribute
1. ActorAttribute v1 一开始我是想做一个可以使用泛型 ScriptableObject 的角色属性 1.1 ScriptableGenericVariable v1 泛型 Scri ...
- [Unity] 战斗系统学习 3:FlowCanvas 中的 Input System
1. 常规 Input System 使用 Assets/MeowFramework/MeowACT/InputSystem/MeowACTInputController.cs // -------- ...
- [Unity] 战斗系统学习 2:FlowCanvas 中的 SubGraph
这篇文章试错出来就是,不改源码实现不了一个 FlowScriptControler 并行执行多个 FlowScript 1. 错误用法 :直接使用 Initialize StartGraph Test ...
最新文章
- 外部排序归并排序 败者树
- AspectJ对AOP的实现
- NYOJ--927--dfs--The partial sum problem
- overflowhidden把内容遮住了怎么办_图片有水印怎么办?不用PS,有这4招就够了!...
- 限制UI只能在屏幕内移动(放大或缩小屏幕同样适用)
- blog微服务架构代码_Spring Cloud微服务架构代码结构详细讲解
- TurboMail邮件服务器帮你应付电子邮件归档危机
- bzoj 4563 [Haoi2016]放棋子 错位排列+高精度
- makefile高级应用
- $《第一行代码:Android》读书笔记——第6章 数据持久化
- 分享一个前后端分离的轻量级内容管理框架
- 年龄血压对照表,每人都应该存一份!
- 2021-03-24
- 财务知识 - 营业税
- 洛阳师范学院计算机组成原理,洛阳师范学院2010-2011-2《计算机组成原理》试卷b(智爱娟).doc...
- latex : 常见编译错误记录
- docker安装报错:docker-ce conflicts with 2:docker-1.13.1-208.git7d71120.el7_9.x86_64
- notepad++怎么解决光标变成下划线的问题?
- 该shi的垃圾短信,为何屡禁不止?有何猫腻?
- 微信恢复大师花了200多,套路一环接一环!
热门文章
- inittab文件剖析[CentOS 5.X](第二版)
- CentOS配置本地YUM源
- .NET MVC Scripts.Render 上下文不存在问题解决方法
- POI不同版本替换Word模板时的问题
- asp.net WebForm页面间传值方法
- android button 行间距,android – 如何减少TextView行间距
- expect java_expect命令
- 信息学奥赛一本通 1188:菲波那契数列(2) | OpenJudge NOI 2.3 1760:菲波那契数列(2)
- 信息学奥赛一本通 2044:【例5.12】回文字串
- 数颜色(洛谷-P1903)