Demo展示

功能介绍

集成了技能,冷却,buff,UI显示,倒计时,动画等;

技能类型:弹道技能,动画事件根据帧数采用延迟调用技能,自定义释放位置(偏移,发射点两种),buff类型技能(自身增益buff,敌人减益buff,比如加防御和毒);

技能伤害判定:碰撞判定,圆形判定(自定义圆心和半径),扇形(角度和半径),线性(长宽),选中目标才可释放;

技能伤害支持多段;

Buff类型:燃烧,减速,感电,眩晕,中毒,击退,击飞,拉拽;增益:回血,加防御;

工具类介绍

CollectionHelper——数组工具,泛型,可以传入数组和条件委托,返回数组中符合条件的所有对象,以及排序功能;

TransformHelper——递归查找指定父节点下所有子节点,返回找到的目标;

SingletonMono——继承了MonoBehaviour的单例;

GameObjectPool——对象池

DamagePopup——掉血数值显示

基类

Skill

技能数据类,所有可以外部导入的技能数据都放在这个类中,以便于可以外部导入数据;

由于测试demo,我另外写了一个SkillTemp类,继承了ScriptaleObject,方便填写测试数据;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// <summary>
/// 技能类型,可叠加
/// </summary>
public enum DamageType
{
Bullet = 4, //特效粒子碰撞伤害
None = 8, //无伤害,未使用,为none可以不选
Buff = 32, //buff技能

//二选一
FirePos = 128, //有发射位置点
FxOffset = 256, //发射偏移,无偏移偏移量为0

//四选一
Circle = 512, //圈判定
Sector = 1024, //扇形判定
Line = 4096, //线性判定
Select = 8192, //选中才可释放
}

DamageType用来确定技能的行为,赋值都是2的倍数,可以使用与或非来减少变量个数;

后来发现直接用List好像也行,后面的Buff就使用了List来存储叠加的情况;

1
2
3
4
5
6
7
8
[CreateAssetMenu(menuName="Create SkillTemp")]
public class SkillTemp : ScriptableObject
{
public Skill skill = new Skill();

/// <summary>技能类型,可用 | 拼接</summary>>
public DamageType[] damageType;
}

继承了ScriptableObject可以右键创建技能模板,直接在inspector界面编辑;

image-20211111010925395

SkillData

组合了Skill类,在Skill类的基础上,添加了更多的不可外部传参的数据;

比如技能特效的引用,技能所有者引用,存储技能攻击目标对象用来在技能模块之间传递,以及技能等级冷却等动态变化的数据;

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
public class SkillData
{
[HideInInspector] public GameObject Owner;

/// <summary>技能数据</summary>
[SerializeField]
public Skill skill;

/// <summary>技能等级</summary>
public int level;

/// <summary>冷却剩余</summary>
[HideInInspector]
public float coolRemain;

/// <summary>攻击目标</summary>
[HideInInspector] public GameObject[] attackTargets;

/// <summary>是否激活</summary>
[HideInInspector]
public bool Activated;

/// <summary>技能预制对象</summary>
[HideInInspector]
public GameObject skillPrefab;

[HideInInspector]
public GameObject hitFxPrefab;
}

CharacterStatus

准确来说这个类不属于技能系统,他用来记录人物属性数据,以及提供受伤,刷新UI条等接口;

同时这个类存储着技能系统必须用到的受击特效挂载点HitFxPos,发射点FirePos,选中Mesh或特效物体selected,伤害数值出现点hudPos,自身头像血条UI物体uiPortrait;

最好是英雄和敌人单独写一个类继承这个基类,但是测试的话这个类就够用了;

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
public class CharacterStatus : MonoBehaviour
{
/// <summary>生命 </summary>
public float HP = 100;
/// <summary>生命 </summary>
public float MaxHP=100;
/// <summary>当前魔法 </summary>
public float SP = 100;
/// <summary>最大魔法 </summary>
public float MaxSP =100;
/// <summary>伤害基数</summary>
public float damage = 100;
///<summary>命中</summary>
public float hitRate = 1;
///<summary>闪避</summary>
public float dodgeRate = 1;
/// <summary>防御</summary>
public float defence = 10f;
/// <summary>主技能攻击距离 ,用于设置AI的攻击范围,与目标距离此范围内发起攻击</summary>
public float attackDistance = 2;
/// <summary>受击特效挂点 挂点名为HitFxPos </summary>
[HideInInspector]
public Transform HitFxPos;
[HideInInspector]
public Transform FirePos;

public GameObject selected;

private GameObject damagePopup;
private Transform hudPos;

public UIPortrait uiPortrait;

public virtual void Start()
{
if (CompareTag("Player"))
{
uiPortrait = GameObject.FindGameObjectWithTag("HeroHead").GetComponent<UIPortrait>();
}
else if (CompareTag("Enemy"))
{
Transform canvas = GameObject.FindGameObjectWithTag("Canvas").transform;
uiPortrait = Instantiate(Resources.Load<GameObject>("UIEnemyPortrait"), canvas).GetComponent<UIPortrait>();
uiPortrait.gameObject.SetActive(false);
//存储所有的uiPortarit在单例中
MonsterMgr.I.AddEnemyPortraits(uiPortrait);
}
uiPortrait.cstatus = this;
//更新血蓝条
uiPortrait.RefreshHpMp();

damagePopup = Resources.Load<GameObject>("HUD");
//初始化数据
selected = TransformHelper.FindChild(transform, "Selected").gameObject;
HitFxPos = TransformHelper.FindChild(transform, "HitFxPos");
FirePos = TransformHelper.FindChild(transform, "FirePos");
hudPos = TransformHelper.FindChild(transform, "HUDPos");
}

/// <summary>受击 模板方法</summary>
public virtual void OnDamage(float damage, GameObject killer,bool isBuff = false)
{
//应用伤害
var damageVal = ApplyDamage(damage, killer);

//应用PopDamage
DamagePopup pop = Instantiate(damagePopup).GetComponent<DamagePopup>();
pop.target = hudPos;
pop.transform.rotation = Quaternion.identity;
pop.Value = damageVal.ToString();

//ApplyUI画像
if (!isBuff)
{
uiPortrait.gameObject.SetActive(true);
uiPortrait.transform.SetAsLastSibling();
uiPortrait.RefreshHpMp();
}
}

/// <summary>应用伤害</summary>
public virtual float ApplyDamage(float damage, GameObject killer)
{
HP -= damage;
//应用死亡
if (HP <= 0)
{
HP = 0;
Destroy(killer, 5f);
}

return damage;
}
}

IAttackSelector

目标选择器接口,只定义了一个方法,选择符合条件的目标并返回;

1
2
3
4
5
6
7
//策略模式 将选择算法进行抽象
/// <summary>攻击目标选择算法</summary>
public interface IAttackSelector
{
///<summary>目标选择算法</summary>
GameObject[] SelectTarget(SkillData skillData, Transform skillTransform);
}

LineAttackSelector,CircleAttackSelector,SectorAttackSelector线性,圆形,扇形目标选择器,继承该接口;

就只展示一个了CircleAttackSelector;

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
class CircleAttackSelector : IAttackSelector
{
public GameObject[] SelectTarget(SkillData skillData, Transform skillTransform)
{
//发一个球形射线,找出所有碰撞体
var colliders = Physics.OverlapSphere(skillTransform.position, skillData.skill.attackDisntance);
if (colliders == null || colliders.Length == 0) return null;

//通过碰撞体拿到所有的gameobject对象
String[] attTags = skillData.skill.attckTargetTags;
var array = CollectionHelper.Select<Collider, GameObject>(colliders, p => p.gameObject);
//挑选出对象中能攻击的,血量大于0的
array = CollectionHelper.FindAll<GameObject>(array,
p => Array.IndexOf(attTags, p.tag) >= 0
&& p.GetComponent<CharacterStatus>().HP > 0);

if (array == null || array.Length == 0) return null;

GameObject[] targets = null;
//根据技能是单体还是群攻,决定返回多少敌人对象
if (skillData.skill.attackNum == 1)
{
//将所有的敌人,按与技能的发出者之间的距离升序排列,
CollectionHelper.OrderBy<GameObject, float>(array,
p => Vector3.Distance(skillData.Owner.transform.position, p.transform.position));
targets = new GameObject[] {array[0]};
}
else
{
int attNum = skillData.skill.attackNum;
if (attNum >= array.Length)
targets = array;
else
{
for (int i = 0; i < attNum; i++)
{
targets[i] = array[i];
}
}
}

return targets;
}
}

这里有个问题,技能的目标选择器每次释放技能都会调用,因此会重复频繁的创建,但其实这只是提供方法而已;

解决:使用工厂来缓存目标选择器;

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
//简单工厂  
//创建敌人选择器
public class SelectorFactory
{
//攻击目标选择器缓存
private static Dictionary<string, IAttackSelector> cache = new Dictionary<string, IAttackSelector>();

public static IAttackSelector CreateSelector(DamageMode mode)
{
//没有缓存则创建
if (!cache.ContainsKey(mode.ToString()))
{
var nameSpace = typeof(SelectorFactory).Namespace;
string classFullName = string.Format("{0}AttackSelector", mode.ToString());

if (!String.IsNullOrEmpty(nameSpace))
classFullName = nameSpace + "." + classFullName;

Type type = Type.GetType(classFullName);
cache.Add(mode.ToString(), Activator.CreateInstance(type) as IAttackSelector);
}

//从缓存中取得创建好的选择器对象
return cache[mode.ToString()];
}
}

小结

所有基类,前期准备数据只有这些,另外想Demo更有体验感,还需要有角色控制,相机跟随脚本;

之后就是技能管理系统,技能释放器等;

技能管理和释放

CharacterSkillSystem

技能系统类,给外部(技能按钮,按键)提供技能释放方法;

技能释放逻辑:

image-20211111122441848

按顺序判定条件,成立则继续,否则返回;

最终调用CharacterSkillManager中的DeploySkill方法;传递参数为SkillData;

提供了随机技能方法;

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
140
141
142
143
144
145
146
147
148
149
150
151
/// <summary>
/// 角色系统
/// </summary>
[RequireComponent(typeof(CharacterSkillManager))]
public class CharacterSkillSystem : MonoBehaviour
{
//技能管理
public CharacterSkillManager chSkillMgr;

//角色状态
private CharacterStatus chStatus;

//角色动画
private Animator mAnimator;

//当前使用的技能
private SkillData currentUseSkill;

//当前攻击的目标
private Transform currentSelectedTarget;

//初始化
public void Start()
{
mAnimator = GetComponent<Animator>();
chSkillMgr = GetComponent<CharacterSkillManager>();
chStatus = GetComponent<CharacterStatus>();
}

/// <summary>
/// 使用指定技能
/// </summary>
/// <param name="skillid">技能编号</param>
/// <param name="isBatter">是否连击</param>
public void AttackUseSkill(int skillid, bool isBatter = false)
{
//如果是连击,找当前技能的下一个连击技能
if (currentUseSkill != null && isBatter)
skillid = currentUseSkill.skill.nextBatterId;
//准备技能
currentUseSkill = chSkillMgr.PrepareSkill(skillid);
if (currentUseSkill != null)
{
//选中释放技能调用
if ((currentUseSkill.skill.damageType & DamageType.Select) == DamageType.Select)
{
var selectedTaget = SelectTarget();
if (currentUseSkill.skill.attckTargetTags.Contains("Player"))
selectedTaget = gameObject;

if (selectedTaget != null)
{
CharacterStatus selectStatus = null;
//修改成获取characterStatus中的Selected节点设置隐藏;
if (currentSelectedTarget != null)
{
selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>();
selectStatus.selected.SetActive(false);
}
currentSelectedTarget = selectedTaget.transform;
selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>();
selectStatus.selected.SetActive(true);

//buff技能
if ((currentUseSkill.skill.damageType & DamageType.Buff) == DamageType.Buff)
{
foreach (var buff in currentUseSkill.skill.buffType)
{
//加bufficon
GameObject uiPortrait = selectStatus.uiPortrait.gameObject;
MonsterMgr.I.HideAllEnemyPortraits();
uiPortrait.SetActive(true);
uiPortrait.transform.SetAsLastSibling();
selectStatus.uiPortrait.AddBuffIcon(buff, currentUseSkill.skill.buffDuration);

//已有该buff刷新
bool exist = false;
var buffs = selectedTaget.GetComponents<BuffRun>();
foreach (var it in buffs)
{
if (it.bufftype == buff)
{
it.Reset();
exist = true;
break;
}
}

if (exist)
continue;

//添加新buff
var buffRun = selectedTaget.AddComponent<BuffRun>();
buffRun.InitBuff(buff, currentUseSkill.skill.buffDuration,
currentUseSkill.skill.buffValue, currentUseSkill.skill.buffInterval);
}
}

//转向目标
//transform.LookAt(currentSelectedTarget);
chSkillMgr.DeploySkill(currentUseSkill);
mAnimator.Play(currentUseSkill.skill.animtionName);
}
}
else
{
chSkillMgr.DeploySkill(currentUseSkill);
mAnimator.Play(currentUseSkill.skill.animtionName);
}
}
}

/// <summary>
/// 随机选择技能
/// </summary>
public void RandomSelectSkill()
{
if (chSkillMgr.skills.Count > 0)
{
int index = UnityEngine.Random.Range(0, chSkillMgr.skills.Count);
currentUseSkill = chSkillMgr.PrepareSkill(chSkillMgr.skills[index].skill.skillID);
if (currentUseSkill == null) //随机技能未找到或未冷却结束
currentUseSkill = chSkillMgr.skills[0]; //用技能表中第一个(默认技能)做补充
}
}

//选择目标
private GameObject SelectTarget()
{
//发一个球形射线,找出所有碰撞体
var colliders = Physics.OverlapSphere(transform.position, currentUseSkill.skill.attackDisntance);
if (colliders == null || colliders.Length == 0) return null;

//从碰撞体列表中挑出所有的敌人
String[] attTags = currentUseSkill.skill.attckTargetTags;
var array = CollectionHelper.Select<Collider, GameObject>(colliders, p => p.gameObject);

//正前方,tag正确,血量大于0,处于正前方的敌人
array = CollectionHelper.FindAll<GameObject>(array,
p => Array.IndexOf(attTags, p.tag) >= 0
&& p.GetComponent<CharacterStatus>().HP > 0 &&
Vector3.Angle(transform.forward, p.transform.position - transform.position) <= 90);

if (array == null || array.Length == 0) return null;

//将所有的敌人,按与技能的发出者之间的距离升序排列,
CollectionHelper.OrderBy<GameObject, float>(array,
p => Vector3.Distance(transform.position, p.transform.position));
return array[0];
}
}

CharacterSkillManager

技能数据的管理,加载所有技能特效模板进入对象池;

给CharacterSkillSystem提供技能释放接口DeploySkill;

提供技能冷却计算,预留获取cd剩余时间接口给UI,以及获取技能是否在cd中;

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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
[RequireComponent(typeof(CharacterSkillSystem))]
public class CharacterSkillManager : MonoBehaviour
{
/// <summary>管理所有技能的容器</summary>
public List<SkillData> skills = new List<SkillData>();

/// <summary>技能的拥有者</summary>
private CharacterStatus chStatus = null;

private SkillData curSkill;

//添加技能数据
private void AddSkill(string path)
{
SkillTemp skTemp = Instantiate(Resources.Load<SkillTemp>(path));
Skill sk = LoadSkill(skTemp);;
SkillData skd = new SkillData();
skd.skill = sk;
skills.Add(skd);
}

//初始化技能数据(有什么技能)
public void Start()
{
chStatus = GetComponent<CharacterStatus>();

AddSkill("Skill_1");
AddSkill("Skill_2");
AddSkill("Skill_3");
AddSkill("Skill_4");
AddSkill("Skill_5");

foreach (var item in skills)
{
//动态加载技能特效预制体 //Resources/Skill -- 技能特效预制体
if (item.skillPrefab == null && !string.IsNullOrEmpty(item.skill.prefabName))
item.skillPrefab = LoadFxPrefab("Skill/" + item.skill.prefabName);

//Resources/Skill/HitFx 技能伤害特效预制体
if (item.hitFxPrefab == null && !string.IsNullOrEmpty(item.skill.hitFxName))
item.hitFxPrefab = LoadFxPrefab("Skill/" + item.skill.hitFxName);
}
}

//将特效预制件载入到对象池,以备将来使用
private GameObject LoadFxPrefab(string path)
{
var key = path.Substring(path.LastIndexOf("/") + 1);
var go = Resources.Load<GameObject>(path);
GameObjectPool.I.Destory(
GameObjectPool.I.CreateObject(
key, go, transform.position, transform.rotation)
);
return go;
}

//准备技能
public SkillData PrepareSkill(int id)
{
//从技能容器中找出相应ID的技能
var skillData = skills.Find(p => p.skill.skillID == id);
if (skillData != null && //查找到技能
chStatus.SP >= skillData.skill.costSP && //检查角色SP是否够使用该技能
skillData.coolRemain == 0) //且该技能已经冷却结束
{
skillData.Owner = gameObject;
return skillData;
}

return null;
}

//释放技能
public void DeploySkill(SkillData skillData)
{
//开始冷却计时
StartCoroutine(CoolTimeDown(skillData));

//动画某一帧触发技能特效,这里写一个延迟调用的方法,使用动画时间的百分解决特效释放时间问题
if (skillData.skill.delayAnimaTime != 0)
{
curSkill = skillData;
Invoke("DelayDeploySkill", skillData.skill.delayAnimaTime);
return;
}

GameObject tempGo = null;
//创建技能预制体+创建位置的偏移
if ((skillData.skill.damageType & DamageType.FxOffset) == DamageType.FxOffset)
tempGo = GameObjectPool.I.CreateObject(skillData.skill.prefabName, skillData.skillPrefab,
transform.position + transform.forward * skillData.skill.fxOffset, transform.rotation);
//技能有发射点
else if ((skillData.skill.damageType & DamageType.FirePos) == DamageType.FirePos)
tempGo = GameObjectPool.I.CreateObject(skillData.skill.prefabName, skillData.skillPrefab,
chStatus.FirePos.position, chStatus.FirePos.rotation);

if(tempGo == null)
return;

//从预制体对象上找到技能释放对象
var deployer = tempGo.GetComponent<SkillDeployer>();
if (deployer == null)
deployer = tempGo.AddComponent<SkillDeployer>();

//设置要释放的技能————划重点
deployer.skillData = skillData;
//调用释放方法
deployer.DeploySkill();

//技能持续时间过后,技能要销毁
if ((skillData.skill.damageType & DamageType.Bullet) != DamageType.Bullet)
{
if (skillData.skill.durationTime > 0)
GameObjectPool.I.Destory(tempGo, skillData.skill.durationTime);
else
GameObjectPool.I.Destory(tempGo, 0.5f);
}
}

//延迟释放技能
private void DelayDeploySkill()
{
GameObject tempGo = null;
//创建技能预制体+创建位置的偏移
if ((curSkill.skill.damageType & DamageType.FxOffset) == DamageType.FxOffset)
tempGo = GameObjectPool.I.CreateObject(curSkill.skill.prefabName, curSkill.skillPrefab,
transform.position + transform.forward * curSkill.skill.fxOffset, transform.rotation);

else if ((curSkill.skill.damageType & DamageType.FirePos) == DamageType.FirePos)
tempGo = GameObjectPool.I.CreateObject(curSkill.skill.prefabName, curSkill.skillPrefab,
chStatus.FirePos.position, chStatus.FirePos.rotation);

//从预制体对象上找到技能释放对象
var deployer = tempGo.GetComponent<SkillDeployer>();
if (deployer == null)
deployer = tempGo.AddComponent<SkillDeployer>();

//设置要释放的技能
deployer.skillData = curSkill;
//调用释放方法
deployer.DeploySkill();

//技能持续时间过后,技能要销毁
if ((curSkill.skill.damageType & DamageType.Bullet) != DamageType.Bullet)
{
if (curSkill.skill.durationTime > 0)
GameObjectPool.I.Destory(tempGo, curSkill.skill.durationTime);
else
GameObjectPool.I.Destory(tempGo, 0.5f);
}
}

//冷却时间倒计时
public IEnumerator CoolTimeDown(SkillData skillData)
{
skillData.coolRemain = skillData.skill.coolTime;
while (skillData.coolRemain > 0)
{
yield return new WaitForSeconds(0.1f);
skillData.coolRemain -= 0.1f;
}

skillData.coolRemain = 0;
}

//取得冷却倒计时的剩余时间(秒)
public float GetSkillCoolRemain(int id)
{
return skills.Find(p => p.skill.skillID == id).coolRemain;
}

private Skill LoadSkill(SkillTemp skillTemp)
{
Skill sk = skillTemp.skill;
int count = skillTemp.damageType.Length;
for (int i = 0; i < count; ++i)
{
sk.damageType = sk.damageType | skillTemp.damageType[i];
}
return sk;
}
}

SkillDeployer

挂载在技能特效上, 执行技能对释放者造成的影响(消耗MP,刷新MPUI);

对命中目标执行伤害计算,加载受伤特效添加debuff等;

伤害触发分为碰撞触发和目标选择器选中触发;

上面划得重点:

给技能释放器中skillData属性赋值的同时,创建目标选择器,给CharacterStatrus字段赋值;

中间有很多坑点:

1.刷新敌人头像显示,必须要设置显示层级在ui的最下层,同时设置其他UI位置,不能设置Active,禁用buff倒计时计算会失效,也可以将buff倒计时单独管理;

2.检测已有相同buff存在刷新buff时间;

3.多段伤害,每段伤害要重新检测攻击目标,有击退等buff存在;

4.伤害计算单独写方法,方便修改;

5.弹道和碰撞触发伤害的技能,受击特效挂载点不应该是HitFxPos,而是碰撞的接触点,然而使用触发器碰撞没办法返回碰撞点坐标,所以又做了射线检测;但是又会存在新的问题,射线检测只有一条线,没有体积,会造成边缘碰撞时射线未检测到,却已经触发碰撞了;

这里做了处理,射线未检测到却碰撞特效生成在HitFxPos;

可以自行尝试一下在技能特效的前段设置HitFxPos来设置受击特效的位置;

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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
public class SkillDeployer : MonoBehaviour
{
private SkillData m_skillData;

///<summary>敌人选区,选择目标的算法</summary>
public IAttackSelector attackTargetSelector;

private DamageMode damageMode;

//发出者
private CharacterStatus status;

/// <summary> 要释放的技能 </summary>
public SkillData skillData
{
set
{
m_skillData = value;
damageMode = 0;
if ((skillData.skill.damageType & DamageType.Sector) == DamageType.Sector)
damageMode = DamageMode.Sector;
else if ((skillData.skill.damageType & DamageType.Circle) == DamageType.Circle)
damageMode = DamageMode.Circle;
else if ((skillData.skill.damageType & DamageType.Line) == DamageType.Line)
damageMode = DamageMode.Line;

if (damageMode != 0)
attackTargetSelector = SelectorFactory.CreateSelector(damageMode);

status = value.Owner.GetComponent<CharacterStatus>();
}
get { return m_skillData; }
}


/// <summary>技能释放</summary>
public virtual void DeploySkill()
{
if (m_skillData == null) return;
//对自身的影响
SelfImpact(m_skillData.Owner);

//执行伤害的计算
if (damageMode != 0)
StartCoroutine(ExecuteDamage());
}

//执行伤害的计算
protected virtual IEnumerator ExecuteDamage()
{
//按持续时间及,两次伤害间隔,
float attackTimer = 0; //已持续攻击的时间

ResetTargets();
if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
{
//Debug.Log(skillData.attackTargets[0].name);
foreach (var item in skillData.attackTargets)
{
//刷新敌人头像显示
CharacterStatus targetStatus = item.GetComponent<CharacterStatus>();
GameObject uiPortrait = targetStatus.uiPortrait.gameObject;
MonsterMgr.I.HideAllEnemyPortraits();
uiPortrait.SetActive(true);
uiPortrait.transform.SetAsLastSibling();

//加buff
foreach (var buff in skillData.skill.buffType)
{
//加bufficon
targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);

//已有该buff刷新
bool exist = false;
var buffs = item.GetComponents<BuffRun>();

foreach (var it in buffs)
{
if (it.bufftype == buff)
{
it.Reset();
exist = true;
break;
}
}

if (exist)
{
continue;
}

//添加新buff
var buffRun = item.AddComponent<BuffRun>();
buffRun.InitBuff(buff, skillData.skill.buffDuration, skillData.skill.buffValue,
skillData.skill.buffInterval);
}
}
}

do
{
//通过选择器选好攻击目标
ResetTargets();
if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
{
//Debug.Log(skillData.attackTargets[0].name);
foreach (var item in skillData.attackTargets)
{
//对敌人的影响
TargetImpact(item);
}
}

yield return new WaitForSeconds(skillData.skill.damageInterval);
attackTimer += skillData.skill.damageInterval;
//做伤害数值的计算
} while (skillData.skill.durationTime > attackTimer);
}

private void ResetTargets()
{
if (m_skillData == null)
return;

m_skillData.attackTargets = attackTargetSelector.SelectTarget(m_skillData, transform);
}

private float CirculateDamage(GameObject goTarget)
{
CharacterStatus goStatus = goTarget.GetComponent<CharacterStatus>();

//是否命中计算
float rate = status.hitRate / (float) goStatus.dodgeRate;
if (rate < 1)
{
int max = (int) (rate * 100);
int val = Random.Range(0, 100);
if (val < max)
{
//Debug.Log("Miss");
return 0;
}
}

//普攻的技能伤害为0; 技能有固定伤害*等级加成 + 普攻伤害
var damageVal = status.damage * (1000 / (1000 + goStatus.defence)) +
skillData.skill.damage * (1 + skillData.level * skillData.skill.damageRatio);
return damageVal;
}

///对敌人的影响nag
public virtual void TargetImpact(GameObject goTarget)
{
//出受伤特效
if (skillData.hitFxPrefab != null)
{
//找到受击特效的挂点
Transform hitFxPos = goTarget.GetComponent<CharacterStatus>().HitFxPos;

var go = GameObjectPool.I.CreateObject(
skillData.skill.hitFxName,
skillData.hitFxPrefab,
hitFxPos.position,
hitFxPos.rotation);
go.transform.SetParent(hitFxPos);
GameObjectPool.I.Destory(go, 2f);
}

//受伤
var damageVal = CirculateDamage(goTarget);
var targetStatus = goTarget.GetComponent<CharacterStatus>();
targetStatus.OnDamage((int) damageVal, skillData.Owner);
}

//碰撞触发目标影响
public virtual void TargetImpact(GameObject goTarget, Collider collider)
{
//敌人buff
foreach (var buff in skillData.skill.buffType)
{
//已有该buff刷新
bool exist = false;
var buffs = goTarget.GetComponents<BuffRun>();
foreach (var it in buffs)
{
if (it.bufftype == buff)
{
it.Reset();
exist = true;
break;
}
}

if (exist)
continue;

//添加新buff
var buffRun = goTarget.AddComponent<BuffRun>();
buffRun.InitBuff(buff, skillData.skill.buffDuration,
skillData.skill.buffValue, skillData.skill.buffInterval);
}



//出受伤特效
if (skillData.hitFxPrefab != null)
{
//找到受击特效的挂点,碰撞但未检测到射线点,生成受击特效在hitFxPos处
Ray ray = new Ray(transform.position, transform.forward);
RaycastHit hit;
Physics.Raycast((Ray) ray, out hit, 1000);
if (hit.collider == collider)
{
var go = GameObjectPool.I.CreateObject(
skillData.skill.hitFxName,
skillData.hitFxPrefab,
hit.point,
transform.rotation);
GameObjectPool.I.Destory(go, 2f);
}
else
{
Transform hitFxPos = goTarget.GetComponent<CharacterStatus>().HitFxPos;
var go = GameObjectPool.I.CreateObject(
skillData.skill.hitFxName,
skillData.hitFxPrefab,
hitFxPos.position,
hitFxPos.rotation);
GameObjectPool.I.Destory(go, 2f);
}
}

//受伤
var damageVal = CirculateDamage(goTarget);
var targetStatus = goTarget.GetComponent<CharacterStatus>();
targetStatus.OnDamage((int) damageVal, skillData.Owner);
}

///对自身的影响
public virtual void SelfImpact(GameObject goSelf)
{
//释放者: 消耗SP
var chStaus = goSelf.GetComponent<CharacterStatus>();
if (chStaus.SP != 0)
{
chStaus.SP -= m_skillData.skill.costSP;
chStaus.uiPortrait.RefreshHpMp();
//add+2 魔法条更新
}
}

private void OnTriggerEnter(Collider other)
{
if ((skillData.skill.damageType & DamageType.Bullet) == DamageType.Bullet)
{
if (skillData.skill.attckTargetTags.Contains(other.tag))
{
if (skillData.skill.attackNum == 1)
{
CharacterStatus targetStatus = other.GetComponent<CharacterStatus>();
GameObject uiPortrait = targetStatus.uiPortrait.gameObject;
MonsterMgr.I.HideAllEnemyPortraits();
uiPortrait.SetActive(true);
uiPortrait.transform.SetAsLastSibling();

//加buff
foreach (var buff in skillData.skill.buffType)
{
//加bufficon
targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);
}

TargetImpact(other.gameObject, other);
}
else
{
//通过选择器选好攻击目标
IAttackSelector selector = new CircleAttackSelector();
selector.SelectTarget(m_skillData, transform);
if (skillData.attackTargets != null && skillData.attackTargets.Length > 0)
{
foreach (var item in skillData.attackTargets)
{
//刷新敌人头像显示
CharacterStatus targetStatus = item.GetComponent<CharacterStatus>();
GameObject uiPortrait = targetStatus.uiPortrait.gameObject;
MonsterMgr.I.HideAllEnemyPortraits();
uiPortrait.SetActive(true);
uiPortrait.transform.SetAsLastSibling();

//加buff
foreach (var buff in skillData.skill.buffType)
{
//加bufficon
targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration);
}

//对敌人的影响
TargetImpact(item, other);
}
}
}

GameObjectPool.I.Destory(gameObject);
}
else if (other.CompareTag("Wall"))
{
if (skillData.hitFxPrefab != null)
{
Ray ray = new Ray(transform.position, transform.forward);
RaycastHit hit;
Physics.Raycast((Ray) ray, out hit, 1000);

if (hit.collider != other)
return;

//找到受击特效的挂点
var go = GameObjectPool.I.CreateObject(
skillData.skill.hitFxName,
skillData.hitFxPrefab,
hit.point,
other.transform.rotation);
//go.transform.SetParent(hitFxPos);
GameObjectPool.I.Destory(go, 2f);
}

GameObjectPool.I.Destory(gameObject);
}
}
}


public static Dictionary<BuffType, string> buffIconName = new Dictionary<BuffType, string>();

public static void InitBuffIconName()
{
buffIconName.Add(BuffType.Burn,"Buff_13");
buffIconName.Add(BuffType.Slow,"Buff_15");
buffIconName.Add(BuffType.Stun,"Buff_12");
buffIconName.Add(BuffType.Poison,"Buff_14");
buffIconName.Add(BuffType.BeatBack,"Buff_5");
buffIconName.Add(BuffType.BeatUp,"Buff_4");
buffIconName.Add(BuffType.Pull,"Buff_6");
buffIconName.Add(BuffType.AddDefence,"Buff_3");
buffIconName.Add(BuffType.RecoverHp,"Buff_7");
buffIconName.Add(BuffType.Light,"Buff_8");
}
}

小结

到目前,所有技能逻辑都结束;下一节介绍buff系统和UI显示相关;

Buff系统

buff分为增益和减益buff,应该区分开来;

本来计划是也用与或非来记录buff的,一个技能可能有多个buff,但是好像用list来存储也是一样的;

一个技能只能有两个buff图标,一个增益buff给自身,一个减益buff给敌人;

一个技能的增益和减益buff可能有多重效果;

比如:技能闪电——导致减速+感电+击退+自身增加狂暴(变态技能);

但是说这么说,写起来比较麻烦,就不那么细分了,一种效果一个图标单独计时;

这里面需求比较复杂,根据需求自行改写吧;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>
/// Buff类型,可叠加
/// </summary>
public enum BuffType
{
None,
Burn = 2, //点燃
Slow = 4, //减速
Light = 8, //感电
Stun = 16, //眩晕
Poison = 32, //中毒
BeatBack = 64, //击退
BeatUp = 128, //击飞
Pull = 256, //拉拽
AddDefence = 512,
RecoverHp = 1024,
}

BuffRun

挂载在拥有buff的物体上,计算buff的效果,如减伤,掉血,减速等;

同时负责buff计时,提供buff计时刷新接口供重复buffIcon调用(也可叠加buff层数按需求);

使用了静态方法初始化和静态链表存储了buff特效的信息,用来动态加载buff特效预制体;

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
public class BuffRun : MonoBehaviour
{
private float durationTime;
public BuffType bufftype;
private float value; //伤害或者加成

private float interval;

private float attackTimer;

private float curTime;

private CharacterStatus target;

//添加buff时候初始化buffrun
public void InitBuff(BuffType buffType,float duration,float value,float interval)
{
bufftype = buffType;

if (buffType == BuffType.BeatBack || buffType == BuffType.BeatUp || buffType == BuffType.Pull)
duration = 2f;

durationTime = duration;
this.value = value;
this.interval = interval;
curTime = 0;
}

//重置buff时间
public void Reset()
{
attackTimer = 0;
curTime = 0;
}

void Start()
{
curTime = 0;
target = GetComponent<CharacterStatus>();
StartCoroutine(ExcuteDamage());
}

private void Update()
{
curTime += Time.deltaTime;

if(curTime > durationTime)
Destroy(this);
}

//执行buff效果,支持多段影响
private IEnumerator ExcuteDamage()
{
attackTimer = 0; //已持续攻击的时间

do
{
//对敌人的影响
TargetImpact();

yield return new WaitForSeconds(interval);
attackTimer += interval;
//做伤害数值的计算
} while (durationTime > attackTimer);

Destroy(this);
}

private void TargetImpact()
{
//buff特效挂载点,有些buff挂载不在HitFxPos,所以写在上面
Transform fxPosTf = target.HitFxPos;

//根据不同buff做相应的效果响应
if (bufftype == BuffType.Burn || bufftype == BuffType.Poison || bufftype == BuffType.Light)
target.OnDamage(value, gameObject, true);
else if (bufftype == BuffType.Slow)//减速
fxPosTf = target.transform;
else if (bufftype == BuffType.BeatBack)
{
Vector3 dir = -target.transform.position + GameObject.FindGameObjectWithTag("Player").transform.position;
dir.y = 0;
target.transform.DOMove(target.transform.position - dir.normalized * value,0.5f);
durationTime = 2f;
}
else if (bufftype == BuffType.BeatUp)
{
target.transform.DOMove(target.transform.position - Vector3.up * value,0.5f);
durationTime = 2f;
}
else if (bufftype == BuffType.AddDefence)
{
fxPosTf = target.transform;
target.defence += value;
}
else if (bufftype == BuffType.RecoverHp)
{
target.OnDamage(-value, gameObject, true);
}

//挂载buff特效
if (buffFx.ContainsKey(bufftype))
{
GameObject go = Resources.Load<GameObject>($"Skill/{buffFx[bufftype]}");
GameObject buffGo = GameObjectPool.I.CreateObject(buffFx[bufftype], go, fxPosTf.position, fxPosTf.rotation);
buffGo.transform.SetParent(fxPosTf);
GameObjectPool.I.Destory(buffGo, interval);
}
}

//存储buff特效名称和对应buff类型
private static Dictionary<BuffType, string> buffFx = new Dictionary<BuffType, string>();
//初始化buff特效信息
public static void InitAllBuff()
{
buffFx.Add(BuffType.Burn,"Skill_32_R_Fly_100");
buffFx.Add(BuffType.Light,"Skill_75_Cast");
buffFx.Add(BuffType.Slow,"Skill_21_R_Fly_100");
buffFx.Add(BuffType.Poison,"Skill_12_R_Fly_100");
buffFx.Add(BuffType.AddDefence,"FX_CHAR_Aura");
buffFx.Add(BuffType.RecoverHp,"FX_Heal_Light_Cast");
}

//获取buff剩余时间接口
public float GetRemainTime()
{
return durationTime - curTime;
}

//buff结束恢复目标属性
private void OnDisable()
{
if (bufftype == BuffType.Slow)
;
else if (bufftype == BuffType.AddDefence)
target.defence -= value;
}
}

BuffIcon

buff图标类,显示倒计时数字显示;

这里写的不是很好,应该加载buffrun的同时加载bufficon,bufficon中不需要单独计时;

暂时改不动了=-=;

bufficon中添加buffRun字段,添加bufficon的同时,赋值buffrun;

通过buffrun获取buff类型和剩余倒计时;

这也用静态方法存储了bufficon的信息,用来动态加载,可以通过外部导入数据来存储;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Dictionary<BuffType, string> buffIconName = new Dictionary<BuffType, string>();

public static void InitBuffIconName()
{
buffIconName.Add(BuffType.Burn,"Buff_13");
buffIconName.Add(BuffType.Slow,"Buff_15");
buffIconName.Add(BuffType.Stun,"Buff_12");
buffIconName.Add(BuffType.Poison,"Buff_14");
buffIconName.Add(BuffType.BeatBack,"Buff_5");
buffIconName.Add(BuffType.BeatUp,"Buff_4");
buffIconName.Add(BuffType.Pull,"Buff_6");
buffIconName.Add(BuffType.AddDefence,"Buff_3");
buffIconName.Add(BuffType.RecoverHp,"Buff_7");
buffIconName.Add(BuffType.Light,"Buff_8");
}

这里写的不太行,参考一下吧;

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
public class BuffIcon : MonoBehaviour
{
public Text textCD;
public Image imgIcon;

private float durationTime;
private float curTime;

public BuffType buffType;

public void LoadIcon(BuffType buffType, float duration)
{
durationTime = duration;
this.buffType = buffType;
Sprite[] temp = Resources.LoadAll<Sprite>("BuffIcon/Buff");
if (temp != null)
{
foreach (var sp in temp)
{
if (sp.name == SkillDeployer.buffIconName[buffType])
{
imgIcon.sprite = Instantiate(sp);
}
}
}
}

private void OnEnable()
{
curTime = 0;
}

void Update()
{
curTime += Time.deltaTime;

textCD.text = (durationTime - curTime).ToString("F0");

if (curTime > durationTime)
{
gameObject.SetActive(false);
curTime = 0;
}
}

public void Refresh()
{
//Debug.Log("已有buff刷新持续时间");
curTime = 0;
}
}

坑点

1.敌人uiPortrait的UI尽量不用使用延迟设置Active来控制显隐藏,Active为false时,bufficon将不再运行,等下次再显示uiPortrait时buff图标会显示错误;

2.采用每次造成伤害将当前目标的uiPortrait调整到显示位置,其他所有敌人的uiPortrait调整位置超出显示区域;

因此需要一个单例来存储所有的uiPortrait;就将它写在MonsterMgr中吧,反正也是用来管理敌人的,顺便管理一下敌人头像也没什么毛病;

提供三个方法,添加uiPortrait,删除uiPortrait,隐藏uiPortrait(移除显示区域);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private List<UIPortrait> allEnemyPortraits = new List<UIPortrait>();
public void AddEnemyPortraits(UIPortrait uiPortrait)
{
allEnemyPortraits.Add(uiPortrait);
}
public void RemoveEnemyPortraits(UIPortrait uiPortrait)
{
allEnemyPortraits.Remove(uiPortrait);
}
public void HideAllEnemyPortraits()
{
foreach (var it in allEnemyPortraits)
{
it.GetComponent<RectTransform>().anchoredPosition = hidePos;
}
}

扇形倒计时

UIbuff图标放置两层image组件物体,父节点设置透明度;

1636621558(1)

子物体image组件按下图设置,填充模式,填充百分比,以及顺逆时针;

1636621295

代码动态设置fillAmount的百分比;

1
2
3
4
5
6
//技能倒计时举例,写在Update中
float cd = csm.skills[i].coolRemain;
skillImgs[i].fillAmount = 1 - cd / csm.skills[i].skill.coolTime;
skillTexts[i].text = cd.ToString("F0");
if (skillTexts[i].text == "0")
skillTexts[i].text = "";

效果:

121212312