Unity之连招系统制作

最近在试图制作连招系统,每次遇到这种逻辑稍微复杂的我都容易头痛,总感觉逻辑好乱啊,于是准备稍微系统性地记录思路,这样更好。


思路

采用“从大到小”的思路:首先我们需要一个连招表,用于记录不同连招逻辑,比如按三下左键打出什么组合。
然后需要思考,什么时候可以攻击,那么这就会引入bool变量CanAttack,先考虑不能攻击的时候:翻墙时、在空中时、被敌人攻击时、格挡时;可以攻击的时候:待机时、跑步时。 接下来考虑我们的攻击冷却时间和攻击距离。先是冷却时间,防止玩家快速点击鼠标键、导致角色攻击动画快速切换造成抽搐效果(比如有人用连点器一秒点999次)。然后是攻击距离,敌人在这个距离内可以被打到播放受伤动画,距离外就不可以。关于冷却时间这点,我们需要自己写个计时器,先做记录
再往下小的方面去考虑,该考虑怎么进行组合呢?我的想法是用ScriptableObject将不同动画文件储存起来,形成一套连招技能配置器
再往小的方面去考虑每个单一的动作,我们需要什么?首先是动作名字ComboName,然后是造成伤害Damage,接着是冷却时间ColdTime,还有攻击距离Distance,此外,每个攻击动作会对应不同的格挡动画和受伤动画,但假如一个动作里给了角色两拳,那么就有两个受伤动画和格挡动画了,这就用数组来表示。
再思考每次受伤伤害是Damage,多次受伤应该是Damage * n,那么就需要一个int值来表示一个动作文件给角色造成几次伤害。
为了方便配置,我们依旧让每个动作继承ScriptableObject,通过以下脚本去进行配置单个动作,代码如下写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;

[CreateAssetMenu(fileName = "ComboData", menuName = "Scriptable Objects/ComboData")]
public class ComboData : ScriptableObject
{
[SerializeField] private string _ComboName;
[SerializeField] private float _Damage;
[SerializeField] private float _ColdTime;
[SerializeField] private string[] _ComboHitName;//比如一个动画文件有两个动作,因此设置数组对应受伤动画和格挡动画
[SerializeField] private string[] _ComboParryName;
[SerializeField] private float _ComboOffsetPostion;//动画距离
//我们给类配置一些参数(因为我们要用),以便使用时能直接类名后加点号点出来
public string ComboName => _ComboName;
public float Damage => _Damage;
public float ColdTime => _ColdTime;
public string[] ComboHitName => _ComboHitName;
public string[] ComboParryName => _ComboParryName;
public float ComboOffsetPostion => _ComboOffsetPostion;
public int ComboHitMax => _ComboHitName.Length;
}

关于ColdTime我们会在另一篇文章里写一个计时器。


接下来我们需要一个技能编辑器,将我们不同动作拖入编辑器里边、来实现技能组合。于是我们就新建个继承ScriptableObject的脚本,将我们的动作拖入Inspector窗口实现编辑

技能的存储我们就用List来存起来,通过引入索引值index来判断我们出于什么动画下。同时给技能编辑器写一些方法:

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
using NUnit.Framework;
using NUnit.Framework.Internal;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

[CreateAssetMenu(fileName = "Combo", menuName = "Scriptable Objects/Combo")]
public class Combo : ScriptableObject
{
[SerializeField]private List<ComboData> comboData = new List<ComboData>();
private int index;
public string TryGetComboName(int index)
{
if(comboData.Count == 0) return null;
return comboData[index].ComboName;
}

public string TryGetHitName(int index,int hitindex)
{
if (comboData.Count == 0) return null;
if (comboData[index].ComboHitMax == 0) return null;
return comboData[index].ComboHitName[hitindex];
}

public string TryGetParryName(int index,int hitindex)
{
if (comboData.Count == 0) return null;
if (comboData[index].ComboHitMax == 0) return null;
return comboData[index].ComboParryName[hitindex];
}

public float TryGetColdTime(int index)
{
if (comboData.Count == 0) return 0;
return comboData[index].ColdTime;
}

public int TryGetHitCount(int index)
{
if (comboData.Count == 0) return 0;
return comboData[index].ComboHitMax;
}

public float TryGetDamage(int index)
{
if (comboData.Count == 0) return 0;
return comboData[index].Damage;
}

public float TryGetComboOffsetPostion(int index)
{
if (comboData.Count == 0) return 0f;
return comboData[index].ComboOffsetPostion;

}
}

接下来,我们专门写一个脚本,与基础角色控制器分开,用来专门记录连招逻辑。

连招逻辑有什么?

1,被攻击、格挡、在空中、攀墙状态下不能攻击;

2,ColdTime内不能攻击(防止抽搐);

3,一段时间不攻击、进入idle状态后,再攻击会从第一招开始。

对于1,我们先写一个返回值为bool的函数吧,起名“CanAttack”;
对于2,在1的基础上补充,启动计时器、必须计时器工作完了、冷却时间过了CanAttack才为true;
对于3,我的想法是用有限状态机的知识,将idle和Attack设置为不同的状态。

未完待续…