Unity skill system (II)
Demo presentation:
5, Skill management and release
1.CharacterSkillSystem
Skill system class, which provides skill release methods for external (skill buttons, keys);
Skill release logic:
Determine the conditions in order. If they are true, how to continue. Otherwise, return;
Finally, call the DeploySkill method in CharacterSkillManager; The transfer parameter is SkillData;
Random skill method is provided;
/// <summary> ///Role system /// </summary> [RequireComponent(typeof(CharacterSkillManager))] public class CharacterSkillSystem : MonoBehaviour { //Skill management public CharacterSkillManager chSkillMgr; //Role status private CharacterStatus chStatus; //Character animation private Animator mAnimator; //Current skills private SkillData currentUseSkill; //Target of current attack private Transform currentSelectedTarget; //initialization public void Start() { mAnimator = GetComponent<Animator>(); chSkillMgr = GetComponent<CharacterSkillManager>(); chStatus = GetComponent<CharacterStatus>(); } /// <summary> ///Use specified skills /// </summary> ///< param name = "skillid" > skill No. < / param > ///< param name = "isbattery" > whether to combo < / param > public void AttackUseSkill(int skillid, bool isBatter = false) { //If it is combo, find the next combo skill of the current skill if (currentUseSkill != null && isBatter) skillid = currentUseSkill.skill.nextBatterId; //Preparation skills currentUseSkill = chSkillMgr.PrepareSkill(skillid); if (currentUseSkill != null) { //Check release skill call 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; //Modify to get the Selected node in characterStatus and set hidden; if (currentSelectedTarget != null) { selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>(); selectStatus.selected.SetActive(false); } currentSelectedTarget = selectedTaget.transform; selectStatus = currentSelectedTarget.GetComponent<CharacterStatus>(); selectStatus.selected.SetActive(true); //buff skill if ((currentUseSkill.skill.damageType & DamageType.Buff) == DamageType.Buff) { foreach (var buff in currentUseSkill.skill.buffType) { //Add bufficon GameObject uiPortrait = selectStatus.uiPortrait.gameObject; MonsterMgr.I.HideAllEnemyPortraits(); uiPortrait.SetActive(true); uiPortrait.transform.SetAsLastSibling(); selectStatus.uiPortrait.AddBuffIcon(buff, currentUseSkill.skill.buffDuration); //The buff has been refreshed 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; //Add new buff var buffRun = selectedTaget.AddComponent<BuffRun>(); buffRun.InitBuff(buff, currentUseSkill.skill.buffDuration, currentUseSkill.skill.buffValue, currentUseSkill.skill.buffInterval); } return; } //Turn to target //transform.LookAt(currentSelectedTarget); chSkillMgr.DeploySkill(currentUseSkill); mAnimator.Play(currentUseSkill.skill.animtionName); } } else { chSkillMgr.DeploySkill(currentUseSkill); mAnimator.Play(currentUseSkill.skill.animtionName); } } } /// <summary> ///Random selection skills /// </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) //Random skill not found or not cooled down currentUseSkill = chSkillMgr.skills[0]; //Supplement with the first (default) skill in the skill table } } //Select target private GameObject SelectTarget() { //Send a spherical ray and find all the colliders var colliders = Physics.OverlapSphere(transform.position, currentUseSkill.skill.attackDisntance); if (colliders == null || colliders.Length == 0) return null; //Pick out all enemies from the collider list String[] attTags = currentUseSkill.skill.attckTargetTags; var array = CollectionHelper.Select<Collider, GameObject>(colliders, p => p.gameObject); //In front, the tag is correct, the HP is greater than 0, and the enemy in front 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; //Rank all enemies in ascending order of distance from the skill sender, CollectionHelper.OrderBy<GameObject, float>(array, p => Vector3.Distance(transform.position, p.transform.position)); return array[0]; } }
2.CharacterSkillManager
Manage skill data, load all skill effect templates into the object pool;
Provide a skill release interface DeploySkill to CharacterSkillSystem;
Provide skill cooling calculation, reserve the remaining time interface for acquiring cd to UI, and whether the acquired skill is in cd;
[RequireComponent(typeof(CharacterSkillSystem))] public class CharacterSkillManager : MonoBehaviour { ///< summary > container for managing all skills < / summary > public List<SkillData> skills = new List<SkillData>(); ///< summary > skill owner < / summary > private CharacterStatus chStatus = null; private SkillData curSkill; //Add skill data 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); } //Initialize skill data (what skills do you have) 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) { //Dynamically load skill effect preform / / Resources/Skill -- skill effect preform if (item.skillPrefab == null && !string.IsNullOrEmpty(item.skill.prefabName)) item.skillPrefab = LoadFxPrefab("Skill/" + item.skill.prefabName); //Resources/Skill/HitFx skill damage effect prefab if (item.hitFxPrefab == null && !string.IsNullOrEmpty(item.skill.hitFxName)) item.hitFxPrefab = LoadFxPrefab("Skill/" + item.skill.hitFxName); } } //Load the effect preform into the object pool for future use 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; } //Preparation skills public SkillData PrepareSkill(int id) { //Find the skill with the corresponding ID from the skill container var skillData = skills.Find(p => p.skill.skillID == id); if (skillData != null && //Skills found chStatus.SP >= skillData.skill.costSP && //Check if the role SP is able to use this skill skillData.coolRemain == 0) //And this skill has been cooled down { skillData.Owner = gameObject; return skillData; } return null; } //Release skills public void DeploySkill(SkillData skillData) { //Start cooling timing StartCoroutine(CoolTimeDown(skillData)); //A frame of animation triggers skill special effects. Here is a delayed call method, which uses the percentage of animation time to solve the problem of special effect release time if (skillData.skill.delayAnimaTime != 0) { curSkill = skillData; Invoke("DelayDeploySkill", skillData.skill.delayAnimaTime); return; } GameObject tempGo = null; //Create skill preform + offset of creation position 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); //Skills have launch points 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; //Find the skill release object from the preform object var deployer = tempGo.GetComponent<SkillDeployer>(); if (deployer == null) deployer = tempGo.AddComponent<SkillDeployer>(); //Set skills to release - Focus deployer.skillData = skillData; //Call release method deployer.DeploySkill(); //After the skill duration, the skill must be destroyed 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); } } //Delayed release skill private void DelayDeploySkill() { GameObject tempGo = null; //Create skill preform + offset of creation position 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); //Find the skill release object from the preform object var deployer = tempGo.GetComponent<SkillDeployer>(); if (deployer == null) deployer = tempGo.AddComponent<SkillDeployer>(); //Set skills to release deployer.skillData = curSkill; //Call release method deployer.DeploySkill(); //After the skill duration, the skill must be destroyed 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); } } //Cooldown countdown 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; } //Gets the remaining time (in seconds) for the cooldown countdown 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; } }
3.SkillDeployer
Mount on the skill effect and execute the impact of the skill on the releaser (consume MP and refresh MPUI);
Perform damage calculation on the hit target, load injury effects, add debuff, etc;
Injury trigger is divided into collision trigger and target selector selected trigger;
Key points drawn above:
While assigning a value to the skillData attribute in the skill releaser, create a target selector and assign a value to the CharacterStatrus field;
There are many pits in the middle:
1. To refresh the enemy's Avatar display, you must set the display level at the lowest level of the UI and set other UI positions. You can't set Active. Disabling the buff countdown calculation will fail. You can also manage the buff countdown separately;
2. It is detected that there is a refresh buff time for the same buff;
3. Multi segment damage. For each segment of damage, the attack target should be re detected, and there are buff s such as repulsion;
4. The injury calculation is written separately to facilitate modification;
5. For the skills of ballistic and collision trigger damage, the mounting point of the hit special effect should not be HitFxPos, but the contact point of the collision. However, the collision with the trigger can not return the coordinates of the collision point, so the ray detection is done again; However, there will be new problems. The ray detection has only one line and no volume, which will cause the edge collision, but the ray is not detected, but the collision has been triggered;
The processing is done here. The collision effect is generated in HitFxPos when the ray is not detected;
You can try setting HitFxPos in the front of the skill effect to set the position of the hit effect;
public class SkillDeployer : MonoBehaviour { private SkillData m_skillData; ///< summary > enemy selection, algorithm for selecting target < / summary > public IAttackSelector attackTargetSelector; private DamageMode damageMode; //Sender private CharacterStatus status; ///< summary > skills to release < / 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 > skill release < / summary > public virtual void DeploySkill() { if (m_skillData == null) return; //Impact on oneself SelfImpact(m_skillData.Owner); //Perform damage calculation if (damageMode != 0) StartCoroutine(ExecuteDamage()); } //Perform damage calculation protected virtual IEnumerator ExecuteDamage() { //According to the duration and interval between two injuries, float attackTimer = 0; //Duration of attack ResetTargets(); if (skillData.attackTargets != null && skillData.attackTargets.Length > 0) { //Debug.Log(skillData.attackTargets[0].name); foreach (var item in skillData.attackTargets) { //Refresh enemy avatar display CharacterStatus targetStatus = item.GetComponent<CharacterStatus>(); GameObject uiPortrait = targetStatus.uiPortrait.gameObject; MonsterMgr.I.HideAllEnemyPortraits(); uiPortrait.SetActive(true); uiPortrait.transform.SetAsLastSibling(); //Add buff foreach (var buff in skillData.skill.buffType) { //Add bufficon targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration); //The buff has been refreshed 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; } //Add new buff var buffRun = item.AddComponent<BuffRun>(); buffRun.InitBuff(buff, skillData.skill.buffDuration, skillData.skill.buffValue, skillData.skill.buffInterval); } } } do { //Select the attack target through the selector ResetTargets(); if (skillData.attackTargets != null && skillData.attackTargets.Length > 0) { //Debug.Log(skillData.attackTargets[0].name); foreach (var item in skillData.attackTargets) { //Impact on the enemy TargetImpact(item); } } yield return new WaitForSeconds(skillData.skill.damageInterval); attackTimer += skillData.skill.damageInterval; //Calculate the damage value } 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>(); //Hit calculation 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; } } //The skill damage of common attack is 0; Skill has fixed damage * level bonus + common attack damage var damageVal = status.damage * (1000 / (1000 + goStatus.defence)) + skillData.skill.damage * (1 + skillData.level * skillData.skill.damageRatio); return damageVal; } ///Impact on the enemy nag public virtual void TargetImpact(GameObject goTarget) { //Injury effects if (skillData.hitFxPrefab != null) { //Find the hanging point of the hit effect 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); } //injured var damageVal = CirculateDamage(goTarget); var targetStatus = goTarget.GetComponent<CharacterStatus>(); targetStatus.OnDamage((int) damageVal, skillData.Owner); } //Impact trigger target public virtual void TargetImpact(GameObject goTarget, Collider collider) { //Enemy buff foreach (var buff in skillData.skill.buffType) { //The buff has been refreshed 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; //Add new buff var buffRun = goTarget.AddComponent<BuffRun>(); buffRun.InitBuff(buff, skillData.skill.buffDuration, skillData.skill.buffValue, skillData.skill.buffInterval); } //Injury effects if (skillData.hitFxPrefab != null) { //Find the hanging point of the hit effect, collide but no ray point is detected, and generate the hit effect at 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); } } //injured var damageVal = CirculateDamage(goTarget); var targetStatus = goTarget.GetComponent<CharacterStatus>(); targetStatus.OnDamage((int) damageVal, skillData.Owner); } ///Impact on oneself public virtual void SelfImpact(GameObject goSelf) { //Released by: consumed SP var chStaus = goSelf.GetComponent<CharacterStatus>(); if (chStaus.SP != 0) { chStaus.SP -= m_skillData.skill.costSP; chStaus.uiPortrait.RefreshHpMp(); //add+2 magic bar update } } 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(); //Add buff foreach (var buff in skillData.skill.buffType) { //Add bufficon targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration); } TargetImpact(other.gameObject, other); } else { //Select the attack target through the selector IAttackSelector selector = new CircleAttackSelector(); selector.SelectTarget(m_skillData, transform); if (skillData.attackTargets != null && skillData.attackTargets.Length > 0) { foreach (var item in skillData.attackTargets) { //Refresh enemy avatar display CharacterStatus targetStatus = item.GetComponent<CharacterStatus>(); GameObject uiPortrait = targetStatus.uiPortrait.gameObject; MonsterMgr.I.HideAllEnemyPortraits(); uiPortrait.SetActive(true); uiPortrait.transform.SetAsLastSibling(); //Add buff foreach (var buff in skillData.skill.buffType) { //Add bufficon targetStatus.uiPortrait.AddBuffIcon(buff, skillData.skill.buffDuration); } //Impact on the enemy 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; //Find the hanging point of the hit effect 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"); } }
Summary
So far, all skill logic is over; The next section describes the buff system and UI display;