之前一直想做一款ACT游戏demo,可惜市面上没找到合适的教程。求人求神不如求己,于是我决定自己开发一款ACT游戏demo,于是记录一下自己的思路。
游戏目标
游戏打算从简单开始设计、慢慢变得更复杂,我的目标就是做出类似于《巫兔》这种,仿《只狼》的ACT游戏。
开发记录
1.相机部分。 首先是关于相机部分,配上InputSystem,写一个GameInputManager类用于管理我们的输入,把GameInputManager写成一个单例,通过鼠标移动控制相机旋转,同时用athf.Clamp设置相机上下看的范围,写一个方法通过鼠标滚轮控制相机与角色距离,实现玩家移动方向朝向相机。
我觉得后续应该会再写一套相机系统,一套索敌用的。因为《巫兔》里打Boss时,相机是不能旋转的,是固定在角色身后的某一方位的,后续再写这个功能。应该再写一个脚本,动态添加相机系统(因为我要写两套)。
2.角色移动。 角色移动我采用的八方向移动+RootMotion,之前踩的坑已经在博客里写过,主要是处理旋转上的逻辑。同时给Animator组件写了个拓展函数,拓展方法AnimationAtTag(string name ,int layerIndex = 0),如果当前动画的标签是name就返回true,不是就返回false,主要用于处理一些逻辑,比如角色在受伤动画下、是不能随相机旋转的,只有AnimationAtTag(“Move”)才能实现跟随相机旋转。
3.角色闪避。 当idle状态下,按下shift会向前翻滚,Run状态下配合方向键做出不同翻滚动作。判断方向就是看我们输入的Vector2向量的x、y值,比如向右翻滚,需要HorizontalSpeed >0f,如果是按下WD+Shift呢,也有考虑,做出向前方翻滚的动作,控制好Vector2的变量就好。 动画状态机
连招系统。 连招实际上是不同动画的组合,这里我借鉴了up主鬼鬼鬼ii的部分连招思路,首先创建两个类,一个是ComboData,配置以下数据: 招式动画名称、招式动画伤害、敌人被这个招式打到的受伤动画的名字、敌人对这个动画格挡动画的名字。接着创建一个类叫Combo,提供游戏方法对ComboData进行管理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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; [SerializeField, Header("这段攻击与目标的最佳距离")] private float _comboOffset; 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 79 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,int hitIndex) { if (_combo.Count == 0 || _combo.Count < index + 1) { return null; } else { return _combo[index].HitName[hitIndex]; } } //获得格挡动画名称 public string TryGetParryName(int index,int hitIndex) { if (_combo.Count == 0 || _combo.Count < index + 1) { return null; } else { return _combo[index].ParryName[hitIndex]; } } //获得招式伤害 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; }
对于连招这方面的处理,先声明一个index作为招式的索引,再声明一个bool类型的方法CanAttackInput(),用于判断是否可以进行攻击输入。如果为true的话,当按下鼠标左键(攻击键)时,index会++,播放下一段动画。 那么CanAttackInput()这个方法怎么写?首先,当AnimationAtTag(“Move”)——角色在移动时候可以攻击(我把Idle动画的标签也改成了Move);然后,动画在过渡状态时不能攻击(animator.IsInTransition)——因为我发现,角色在待机动画向翻滚动画过渡时进行攻击,会发生抽搐现象。最后呢,当前动画播放进度超过60%就能攻击了。 最后处理连招索引的问题,比如角色攻击了一下,长时间不操作、下次攻击也应该从第一段攻击开始,currentIndex需要归为0了。我的思路是,当动画播放完、索引值就归为0;连招是只能在动画的60%-100%这个区间触发的。 展示代码:
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 using GGG.Tool; using UnityEngine; public class PlayerCombo : MonoBehaviour { private Animator animator; [SerializeField,Header("角色组合技")]private Combo currentCombo; [SerializeField, Header("技能失效时间")] private float outTime = 1.4f; //当前招式索引值 private int currentIndex = 0; private void Awake() { animator = GetComponent<Animator>(); } // Update is called once per frame void Update() { Attack(); Reset(); } //是否可以攻击输入 private bool CanAttackInput() { if (animator.AnimationAtTag("Move") && !animator.IsInTransition(0)) return true; if(animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 0.6f) return true; return false; } /// <summary> /// 攻击逻辑书写 /// </summary> private void Attack() { //不能攻击就别执行逻辑了 if (CanAttackInput() == false) return; if(GameInputManager.Instance().attack) { animator.CrossFade(currentCombo.TryGetComboName(currentIndex), 0.1f); currentIndex++; outTime = 999.4f; } if(currentIndex == currentCombo.TryGetComboCount()) { currentIndex = 0; } } private void Reset() { if(animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 0.95f) currentIndex = 0; } }
12.14优化移动逻辑。 优化 这里把状态机优化了一下,把原来的翻滚动画融合成一个混合树,idle和run也融合成一个混合树,把HasInput参数取消掉了,相机旋转部分的逻辑稍微改变,当动画在move状态且输入不等于Vector2.zero时可以跟随相机旋转。