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 public enum DamageType{ Bullet = 4 , None = 8 , Buff = 32 , FirePos = 128 , FxOffset = 256 , 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(); public DamageType[] damageType; }
继承了ScriptableObject可以右键创建技能模板,直接在inspector界面编辑;
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; [SerializeField ] public Skill skill; public int level; [HideInInspector ] public float coolRemain; [HideInInspector ] public GameObject[] attackTargets; [HideInInspector ] public bool Activated; [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 { public float HP = 100 ; public float MaxHP=100 ; public float SP = 100 ; public float MaxSP =100 ; public float damage = 100 ; public float hitRate = 1 ; public float dodgeRate = 1 ; public float defence = 10f ; public float attackDistance = 2 ; [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 ); 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" ); } public virtual void OnDamage (float damage, GameObject killer,bool isBuff = false ) { var damageVal = ApplyDamage(damage, killer); DamagePopup pop = Instantiate(damagePopup).GetComponent<DamagePopup>(); pop.target = hudPos; pop.transform.rotation = Quaternion.identity; pop.Value = damageVal.ToString(); if (!isBuff) { uiPortrait.gameObject.SetActive(true ); uiPortrait.transform.SetAsLastSibling(); uiPortrait.RefreshHpMp(); } } 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 public interface IAttackSelector { 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 ; String[] attTags = skillData.skill.attckTargetTags; var array = CollectionHelper.Select<Collider, GameObject>(colliders, p => p.gameObject); 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 技能系统类,给外部(技能按钮,按键)提供技能释放方法;
技能释放逻辑:
按顺序判定条件,成立则继续,否则返回;
最终调用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 [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>(); } 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 ; if (currentSelectedTarget != null ) { selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>(); selectStatus.selected.SetActive(false ); } currentSelectedTarget = selectedTaget.transform; selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>(); selectStatus.selected.SetActive(true ); if ((currentUseSkill.skill.damageType & DamageType.Buff) == DamageType.Buff) { foreach (var buff in currentUseSkill.skill.buffType) { GameObject uiPortrait = selectStatus.uiPortrait.gameObject; MonsterMgr.I.HideAllEnemyPortraits(); uiPortrait.SetActive(true ); uiPortrait.transform.SetAsLastSibling(); selectStatus.uiPortrait.AddBuffIcon(buff, currentUseSkill.skill.buffDuration); 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 ; var buffRun = selectedTaget.AddComponent<BuffRun>(); buffRun.InitBuff(buff, currentUseSkill.skill.buffDuration, currentUseSkill.skill.buffValue, currentUseSkill.skill.buffInterval); } } chSkillMgr.DeploySkill(currentUseSkill); mAnimator.Play(currentUseSkill.skill.animtionName); } } else { chSkillMgr.DeploySkill(currentUseSkill); mAnimator.Play(currentUseSkill.skill.animtionName); } } } 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); 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 { public List<SkillData> skills = new List<SkillData>(); 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) { if (item.skillPrefab == null && !string .IsNullOrEmpty(item.skill.prefabName)) item.skillPrefab = LoadFxPrefab("Skill/" + item.skill.prefabName); 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 ) { var skillData = skills.Find(p => p.skill.skillID == id); if (skillData != null && chStatus.SP >= skillData.skill.costSP && 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; public IAttackSelector attackTargetSelector; private DamageMode damageMode; private CharacterStatus status; 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; } } 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 ) { 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(); foreach (var buff in skillData.skill.buffType) { targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration); 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 ; } 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 ) { 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) { return 0 ; } } var damageVal = status.damage * (1000 / (1000 + goStatus.defence)) + skillData.skill.damage * (1 + skillData.level * skillData.skill.damageRatio); return damageVal; } 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 ) { foreach (var buff in skillData.skill.buffType) { 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 ; var buffRun = goTarget.AddComponent<BuffRun>(); buffRun.InitBuff(buff, skillData.skill.buffDuration, skillData.skill.buffValue, skillData.skill.buffInterval); } if (skillData.hitFxPrefab != null ) { 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 ) { var chStaus = goSelf.GetComponent<CharacterStatus>(); if (chStaus.SP != 0 ) { chStaus.SP -= m_skillData.skill.costSP; chStaus.uiPortrait.RefreshHpMp(); } } 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(); foreach (var buff in skillData.skill.buffType) { 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(); foreach (var buff in skillData.skill.buffType) { 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); 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 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; 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 ; } 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 ); } private IEnumerator ExcuteDamage () { attackTimer = 0 ; do { TargetImpact(); yield return new WaitForSeconds (interval ) ; attackTimer += interval; } while (durationTime > attackTimer); Destroy(this ); } private void TargetImpact () { Transform fxPosTf = target.HitFxPos; 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 ); } 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); } } private static Dictionary<BuffType, string > buffFx = new Dictionary<BuffType, string >(); 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" ); } public float GetRemainTime () { return durationTime - curTime; } 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 () { 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组件物体,父节点设置透明度;
子物体image组件按下图设置,填充模式,填充百分比,以及顺逆时针;
代码动态设置fillAmount的百分比;
1 2 3 4 5 6 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 = "" ;
效果: