今天试着自己实现了简单的连招系统,先来介绍一下该系统的逻辑:角色按左键是轻攻击、右键是重攻击,重攻击四组、轻攻击三组动画。根据轻攻击连按次数+右键的点击打出不同的重攻击。
技能编辑器
首先做一个最基础的属性编辑器,用ScriptableObject做,为每一个攻击动画配置好我们需要的参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 using UnityEngine; [CreateAssetMenu(fileName = "ComboData", menuName = "Scriptable Objects/ComboData")] public class ComboData : ScriptableObject { [SerializeField, Header("招式名称")] private string comboName; [SerializeField, Header("招式伤害")] private int comboDamage; [SerializeField, Header("被招式受击名称")] private string hitName; [SerializeField, Header("被招式格挡名称")] private string parryName; [SerializeField, Header("招式冷却时间")] private float coldTime; public string ComboName => comboName; public int ComboDamage => comboDamage; public string HitName => hitName; public string ParryName => parryName; public float ColdTime => coldTime; }
上面就是我们给每个攻击动作配置的属性了,光有属性还不够,需要创建技能编辑器来进行组合。我们对外提供一些方法,可以根据索引来快速获取每个动作的参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 using NUnit.Framework; using System.Collections.Generic; using Unity.Burst.Intrinsics; using UnityEngine; [CreateAssetMenu(fileName = "Combo", menuName = "Scriptable Objects/Combo")] public class Combo : ScriptableObject { //显示可编辑的招式 [SerializeField]private List<ComboData> _combo = new List<ComboData>(); public string TryGetComboName(int index) { if (_combo.Count == 0 || _combo.Count < index + 1) { return null; } else { return _combo[index].ComboName; } } //获得受伤动画名称 public string TryGetHitName(int index) { if (_combo.Count == 0 || _combo.Count < index + 1) { return null; } else { return _combo[index].HitName; } } //获得格挡动画名称 public string TryGetParryName(int index) { if (_combo.Count == 0 || _combo.Count < index + 1) { return null; } else { return _combo[index].ParryName; } } //获得招式伤害 public int TryGetComboDamage(int index) { if (_combo.Count == 0 || _combo.Count < index + 1) { return 0; } else { return _combo[index].ComboDamage; } } public float TryGetColdTime(int index) { if(_combo.Count == 0 || _combo.Count < index + 1) { return 0; } else { return _combo[index].ColdTime; } } //获得招式动画数量 public int TryGetComboCount() => _combo.Count; }
之后我们就可以将不同动作进行组合、打出不同的组合技了。
编写核心代码
我的思路是,给出基础组合技和变招表,声明两个索引值分别记录轻攻击索引和重攻击索引。首先声明一个变量canAttck来帮助我们控制角色能否攻击,比如我们在打出第一段攻击后、有0.4秒时间我们是不希望角色进行第二段攻击的(怕点击次数太快造成抽搐),再写一个函数CanAttack()来判断能不能进行攻击。 我们默认使用基础组合技,当按下鼠标左键,comboIndex和changeComboIndex都会递增,动画播放后canAttack = false;之后通过时间管理器 进行延迟调用函数,延迟调用ResetCombo(); 对于重攻击,默认是不能连按右键打出重攻击组合技的,需要配合轻攻击,那就每次打出一个重攻击就把它的索引值归为0。具体参考以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 using UnityEngine; public class PlayerComboController : MonoBehaviour { [SerializeField, Header("基础组合技")] private Combo baseCombo; [SerializeField, Header("变招表")] private Combo ChangeCombo; private Combo currentCombo; private Animator animator; private int comboIndex = 0; private int changeComboIndex = 0; private int currentIndex = 0; private bool canAttack = true; private float coldTime = 0f; /// <summary> /// 上面是连招逻辑的,接下来写相机 /// </summary> [SerializeField, Header("检测球半径")] private float detectSphereRadius; [SerializeField,Header("检测最大距离")]private float detectSphereDistance; private Transform currentEnemy; private Transform _mainCamera; private Vector3 detectDir; private void Awake() { animator = GetComponent<Animator>(); currentCombo = baseCombo; _mainCamera = Camera.main.transform; currentIndex = comboIndex; } private void Update() { ExcuteAttack(); EndAttack(); } private void FixedUpdate() { DetectDirection(); } private void AttackExcute() { //检测范围不对,直接return if (Vector3.Dot(this.transform.forward, ExpandClass.GetTargetVector3(this.transform, currentEnemy)) < 0.85f) return; if(ExpandClass.GetTargetVector3(this.transform,currentEnemy).magnitude > 1.3f) return; //如果攻击了,执行下边逻辑 Vector3 targetDir = -currentEnemy.transform.forward; Quaternion targetRotation = Quaternion.LookRotation(targetDir); transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, 5f * Time.deltaTime); } private void DetectDirection() { //判断检测方向 detectDir = _mainCamera.transform.forward * GameInputManager.Instance().movement.y + _mainCamera.transform.right * GameInputManager.Instance().movement.x; detectDir.Set(detectDir.x,0,detectDir.z); detectDir = detectDir.normalized; if (Physics.SphereCast(this.transform.position + this.transform.up * 0.7f, detectSphereRadius, detectDir, out var hit, detectSphereDistance,1 << 6, QueryTriggerInteraction.Ignore)) { //说明检测到了敌人 currentEnemy = hit.collider.transform; } } private void OnDrawGizmos() { Gizmos.DrawWireSphere((this.transform.position + this.transform.up * 0.7f + detectDir * detectSphereDistance), detectSphereRadius); } #region 连招逻辑 private bool CanAttack() { if (!canAttack) return false; if (animator.AnimationAtTag("Parry")) return false; if (animator.AnimationAtTag("Hit")) return false; return true; } private void ExcuteAttack() { if (!CanAttack()) return; if (GameInputManager.Instance().leftAttack) { currentIndex = comboIndex; currentCombo = baseCombo; animator.CrossFade(currentCombo.TryGetComboName(comboIndex), 0.15f); coldTime = currentCombo.TryGetColdTime(comboIndex); canAttack = false; TimerManager.Instance().TryGetOneTimer(coldTime, ResetCombo); } if(GameInputManager.Instance().RightAttack) { currentIndex = changeComboIndex; currentCombo = ChangeCombo; animator.CrossFade(currentCombo.TryGetComboName(currentIndex),0.1f); coldTime = currentCombo.TryGetColdTime(currentIndex); canAttack = false; changeComboIndex = 0; comboIndex = 0; TimerManager.Instance().TryGetOneTimer(coldTime, ResetHeavy); } } private void ResetCombo() { comboIndex++; changeComboIndex++; canAttack = true; coldTime = 0f; if(comboIndex == currentCombo.TryGetComboCount()) { comboIndex = 0; } //关于这点我解释一下,如果角色打完一套轻击、不停下来又开始打轻击,此时就得重置一下changeComboIndex。你问为什么打重击或者停止时?我们都已经重置了。 //至于为什么是1,因为我们设置的changeComboIndex == 0 时,那个动画只有静止时、或者打完一个重击再打才能用。这里轻击我们采用从1开始的重置 if(changeComboIndex == ChangeCombo.TryGetComboCount()) { changeComboIndex = 1; } } private void ResetHeavy() { canAttack = true; coldTime = 0f; } private void EndAttack() { if (animator.AnimationAtTag("Move") || animator.AnimationAtTag("Idle")) { comboIndex = 0; changeComboIndex = 0; } } #endregion }