# 观前提醒

项目会在 GitHub 中开源,链接:https://github.com/Maikire/Unity/tree/main/UnityFramework/A simple ARPG skill framework

# 做框架之前的一些准备

在开始写框架之前需要有一些准备好的资源

# 角色

  1. 模型和动作
  2. 使用这个角色框架
  3. 基于动画事件行为类给动画添加事件
  4. Animator 如图所示:img

# UI

  1. 使用这个 UI 框架
  2. 使用这个摇杆控制角色的移动
  3. 有关 UI 事件的代码在下文中会讲到
  4. 规定:按钮的名字必须是技能的 ID
  5. UI 如图所示:img img

# 技能预制件

预制件需要放在 Resources 目录中(本文以 Resource 为例,StreamingAssets 同理)
如图:
img

# 选中特效

目标被选中后的特效,统一名字为 SelectTarget

# 需求分析

需求:需要一个 ARPG 游戏的技能框架,实现技能的释放。技能样式可参考以下图片:img

# 技能系统的实现方式

  • 一个技能一个类

    优点:代码编写很简单、可以任意的扩展。

    缺点:代码的重复度很高、数据散乱、技能较多时,代码会变得难以维护和调试

    适用性:适合制作一些内容较少的小游戏

  • 使用框架

    优点:较低的代码重复度、数据集中管理、较高的可扩展性、易于维护和调试、更好的分配任务、更好的并行开发

    缺点:代码编写较难

    适用性:适合制作一些内容较多的游戏

# 框架设计

  • 技能释放流程
    1. 玩家按下按钮
    2. 技能拥有者释放出技能
    3. 技能根据已经设置好的数据以及对应的算法释放
    4. 技能命中受击方,产生影响效果
  • 技能的分类
    1. 释放类型:指定方向、指定目标、指定位置......
    2. 攻击类型:单体攻击、群体攻击......
    3. 选择类型:扇形 / 圆形选择、矩形选择......
  • 实现技能数据的集中管理:用一个类来存放所有的技能数据
  • 角色需要拥有一个存放技能数据的类,提供生成技能的方法
  • 技能需要一个类,用于释放技能(创建并执行算法)
  • 判断技能的命中,不同的技能有不同的算法,使用接口统一管理
  • 技能的影响效果,不同的技能有不同的算法,使用接口统一管理
  • 封装技能系统,对外提供简单的调用方法(两种情况:为玩家使用、为 NPC 使用)

img
img

# 代码

# 技能数据类

用于存放技能的所有数据

SkillData
namespace SkillSystem
{
    [Serializable]
    /// <summary>
    /// 技能数据类
    /// </summary>
    public class SkillData
    {
        [Tooltip("技能ID")]
        public int SkillID;
        [Tooltip("技能名称")]
        public string Name;
        [Tooltip("技能描述")]
        public string Description;
        [Tooltip("冷却时间(秒)")]
        public float CoolTime;
        [Tooltip("冷却剩余(秒)")]
        public float CoolRemain;
        [Tooltip("法力消耗")]
        public float CostMP;
        [Tooltip("攻击距离")]
        public float AttackDistance;
        [Tooltip("攻击角度")]
        public float AttackAngle;
        [Tooltip("移动距离")]
        public float MoveDistance;
        [Tooltip("移动速度")]
        public float MoveSpeed;
        [HideInInspector]
        [Tooltip("起始旋转")]
        public Quaternion StartRotation;
        [HideInInspector]
        [Tooltip("释放起点")]
        public Vector3 StartPosition;
        [Tooltip("攻击目标 的 tag")]
        public string[] AttackTargetTags = { "Enemy" };
        [HideInInspector]
        [Tooltip("攻击目标 的 数组")]
        public Transform[] AttackTargets;
        [HideInInspector]
        [Tooltip("被攻击过的目标")]
        public Dictionary<string, SkillAttackedTarget> AttackedTargets;
        [Tooltip("技能影响类型")]
        public string[] ImpactType = { "CostSP", "Damage" };
        [Tooltip("连击的下一个技能ID")]
        public int NextBatterID;
        [Tooltip("可触发连击/蓄力的最短时间")]
        public float BatterTimeMin;
        [Tooltip("可触发连击/蓄力的最长时间")]
        public float BatterTimeMax;
        [Tooltip("固定伤害")]
        public float AttackDamage;
        [Tooltip("伤害倍率")]
        public float AttackRatio;
        [Tooltip("持续时间")]
        public float DurationTime;
        [Tooltip("伤害间隔")]
        public float AttackInterval;
        [HideInInspector]
        [Tooltip("技能所属")]
        public GameObject Owner;
        [Tooltip("技能预制件名称")]
        public string PrefabName;
        [HideInInspector]
        [Tooltip("技能预制件")]
        public GameObject Prefab;
        [Tooltip("动画名称")]
        public string AnimationName;
        [Tooltip("受击特效名称")]
        public string HitEffectName;
        [HideInInspector]
        [Tooltip("受击特效预制件")]
        public GameObject HitEffectPrefab;
        [Tooltip("技能等级")]
        public int Level;
        [Tooltip("释放类型(指定目标/指定方向/指定位置...)")]
        public SkillDeployType DeployType;
        [Tooltip("攻击类型(单攻/群攻...)")]
        public SkillAttackType AttackType;
        [Tooltip("选择类型(矩形/扇形...)")]
        public SkillSelectorType SelectorType;
    }
}
SkillAttackedTarget
using UnityEngine;
namespace SkillSystem
{    
    /// <summary>
    /// 被攻击过的目标
    /// </summary>
    public class SkillAttackedTarget
    {
        [Tooltip("被攻击过的目标")]
        public Transform Target;
        [Tooltip("被攻击的次数")]
        public int Times;
        public AttackedTarget(Transform Target, int Number)
        {
            this.Target = Target;
            this.Times = Number;
        }
    }
}
SkillDeployType
using UnityEngine;
namespace SkillSystem
{
    public enum SkillDeployType
    {
        [Tooltip("指定目标")]
        Target,
        [Tooltip("指定方向")]
        Direction,
        [Tooltip("指定位置")]
        Location,
    }
}
SkillAttackType
using UnityEngine;
namespace SkillSystem
{
    public enum SkillAttackType
    {
        [Tooltip("单攻")]
        Single,
        [Tooltip("群攻")]
        Group,
    }
}
SkillSelectorType
using UnityEngine;
namespace SkillSystem
{
    public enum SkillSelectorType
    {
        [Tooltip("扇形/圆形")]
        Sector,
    }
}

# 技能管理器

  • 存放所有的技能
  • 初始化技能
    1. 生成资源配置文件
    2. 利用资源管理器,找到预制件
    3. 初始化部分数据
    4. 注:加载预制件以 Resource 为例,StreamingAssets 同理
  • 提供准备技能的方法(判断是否满足技能的释放条件)
  • 利用对象池,提供生成技能的方法,调用技能释放器
  • 处理冷却时间
    • 因为部分技能的存在时间小于冷却时间(技能冷却通过协程实现,物体禁用后代码会停止运行),所以技能冷却需要写在技能管理器中

代码如下

CharacterSkillManager
using Character;
using Common;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace SkillSystem
{
    [RequireComponent(typeof(CharacterStatus))]
    /// <summary>
    /// 技能管理器
    /// </summary>
    public class CharacterSkillManager : MonoBehaviour
    {
        // 本文使用的是手动填写配置
        // 想要通过配置表自动填写数据,只需要额外写一个类实现读取配置数据,然后在这个类中写一个方法给技能数据赋值即可
        [Tooltip("技能列表")]
        public SkillData[] Skills; 
        private CharacterStatus Character; // 角色状态
        private void Awake()
        {
            Character = this.GetComponent<CharacterStatus>();
        }
        private void Start()
        {
            InitSkill();
        }
        /// <summary>
        /// 初始化技能
        /// </summary>
        private void InitSkill()
        {
            foreach (var skill in Skills)
            {
                skill.Prefab = ResourceManager.Load<GameObject>(skill.PrefabName);
                skill.Owner = this.gameObject;
                skill.AttackedTargets = new Dictionary<string, SkillAttackedTarget>();
            }
        }
        /// <summary>
        /// 准备技能
        /// </summary>
        /// <param name="skillID"> 技能 ID</param>
        /// <returns></returns>
        public SkillData PrepareSkill(int skillID)
        {
            SkillData skillData = Skills.Find(s => s.SkillID == skillID);
            if (skillData != null && skillData.CoolRemain <= 0 && Character.MP >= skillData.CostMP)
            {
                return skillData;
            }
            return null;
        }
        /// <summary>
        /// 生成技能
        /// </summary>
        public void GenerateSkill(SkillData skillData, Vector3 position, Quaternion rotation)
        {
            // 生成技能
            GameObject skill = ObjectPool.Instance.GetGameObject(skillData.Name, skillData.Prefab, position, rotation);
            // 技能释放器
            SkillDeployer deployer = skill.GetComponent<SkillDeployer>();
            // 传递技能数据,创建算法对象
            deployer.SkillData = skillData;
            // 执行技能算法
            deployer.DeploySkill();
        }
        /// <summary>
        /// 技能冷却计时器
        /// </summary>
        public IEnumerator CoolTimer(SkillData skillData)
        {
            skillData.CoolRemain = skillData.CoolTime;
            while (skillData.CoolRemain > 0)
            {
                yield return new WaitForSeconds(0.1f);
                skillData.CoolRemain -= 0.1f;
            }
        }
    }
}

# 接口

  • 攻击选区(寻找目标):提供寻找目标的方法
  • 影响效果
    • 部分影响效果有持续时间,所以需要使用协程
    • 普通 CSharp 类无法使用协程,所以这里需要一个 Unity 脚本(挂载到技能的预制件上)
    • 技能释放器会挂载到技能的预制件上,在释放技能时生成影响效果,所以技能释放器是最好的选择
  • 如果有其他需求,可以增加接口

攻击选区(寻找目标)

IAttackSelector
using UnityEngine;
namespace SkillSystem
{
    /// <summary>
    /// 攻击选区
    /// </summary>
    public interface IAttackSelector
    {
        /// <summary>
        /// 搜索目标
        /// </summary>
        /// <param name="skillData"> 技能数据 & lt;/param>
        /// <param name="skillTransform"> 技能的位置 & lt;/param>
        /// <returns></returns>
        Transform[] SelectTarget(SkillData skillData, Transform skillTransform);
    }
}

影响效果

namespace SkillSystem
{
    /// <summary>
    /// 技能影响效果
    /// </summary>
    public interface IImpactEffect
    {
        /// <summary>
        /// 执行影响效果
        /// 部分效果需要用到协程,未继承 MonoBehaviour 的类无法使用协程,
        /// 这里需要一个脚本对象,而技能释放器是最好的选择
        /// </summary>
        /// <param name="skillDeployer"> 技能释放器 & lt;/param>
        void Execute(SkillDeployer skillDeployer);
    }
}

# 技能释放器 和 释放器工厂

  • 技能释放器提供释放技能、寻找目标、造成影响效果、回收技能等方法
  • 技能释放器是抽象类,子类是具体的释放器(例如:指定方向释放器)。子类需要挂载到技能的预制件上。
  • 技能释放器需要调用选区算法、影响效果等接口的实现类。
    • 这些接口的实现类都是普通的 CSharp 类,这就出现了两个问题:由谁来创建这些对象?怎么创建这些对象?
    • 解决方法:可以做一个工厂类。工厂类提供静态方法,使用反射创建对应的实现类。
  • 因为使用反射创建对象,所以需要有命名规则:
    • 选区对象命名规则:SkillSystem. + SelectorType + AttackSelector
    • 影响效果对象命名规则:SkillSystem. + ImpactType + Impact

工厂类的代码如下

DeployerFactory
using System;
using System.Collections.Generic;
namespace SkillSystem
{
    /// <summary>
    /// 释放器工厂
    /// 创建释放器算法
    /// </summary>
    public class DeployerFactory
    {
        /// <summary>
        /// 储存已生成的对象,循环利用
        /// </summary>
        private static Dictionary<string, object> ObjectCache;
        /// <summary>
        /// Type 缓存
        /// </summary>
        private static Dictionary<string, Type> TypeCache;
        static DeployerFactory()
        {
            ObjectCache = new Dictionary<string, object>();
            TypeCache = new Dictionary<string, Type>();
        }
        /// <summary>
        /// 获取类型
        /// </summary>
        /// <param name="typeName"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        private static Type GetType(string typeName)
        {
            if (TypeCache.ContainsKey(typeName))
            {
                return TypeCache[typeName];
            }
            Type type = Type.GetType(typeName);
            if (type == null)
            {
                throw new Exception($"找不到类型: {typeName}");
            }
            TypeCache.Add(typeName, type);
            return type;
        }
        /// <summary>
        /// 创建对象
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="className"></param>
        /// <returns></returns>
        private static T CreateObject<T>(string className) where T : class
        {
            if (ObjectCache.ContainsKey(className))
            {
                return ObjectCache[className] as T;
            }
            else
            {
                Type skillType = GetType(className);
                object temp = Activator.CreateInstance(skillType);
                ObjectCache.Add(className, temp);
                return temp as T;
            }
        }
        /// <summary>
        /// 创建攻击选区
        /// </summary>
        /// <param name="skillData"></param>
        /// <returns></returns>
        public static IAttackSelector CreateAttackSelector(SkillData skillData)
        {
            // 选区对象命名规则:SkillSystem. + SelectorType + AttackSelector
            string selectorClassName = String.Format("SkillSystem.{0}AttackSelector", skillData.SelectorType);
            return CreateObject<IAttackSelector>(selectorClassName);
        }
        /// <summary>
        /// 创建影响效果
        /// </summary>
        /// <param name="skillData"></param>
        /// <returns></returns>
        public static IImpactEffect[] CreateImpactEffects(SkillData skillData)
        {
            // 影响效果对象命名规则:SkillSystem. + ImpactType [i] + Impact
            IImpactEffect[] Impacts = new IImpactEffect[skillData.ImpactType.Length];
            for (int i = 0; i < skillData.ImpactType.Length; i++)
            {
                string impactClassName = String.Format("SkillSystem.{0}Impact", skillData.ImpactType[i]);
                Impacts[i] = CreateObject<IImpactEffect>(impactClassName);
            }
            return Impacts;
        }
    }
}

技能释放器的代码如下

SkillDeployer
using UnityEngine;
namespace SkillSystem
{
    /// <summary>
    /// 技能释放器
    /// </summary>
    public abstract class SkillDeployer : MonoBehaviour
    {
        /// <summary>
        /// 由技能管理器提供数据
        /// </summary>
        private SkillData skillData;
        public SkillData SkillData
        {
            get
            {
                return skillData;
            }
            set
            {
                skillData = value;
                InitDeployer();
            }
        }
        // 算法对象
        private IAttackSelector Selector;
        private IImpactEffect[] Impacts;
        /// <summary>
        /// 初始化释放器
        /// </summary>
        public void InitDeployer()
        {
            // 攻击选区
            Selector = DeployerFactory.CreateAttackSelector(skillData);
            // 影响效果
            Impacts = DeployerFactory.CreateImpactEffects(skillData);
        }
        /// <summary>
        /// 选区(寻找目标)
        /// </summary>
        public void CalculateTargets()
        {
            skillData.AttackTargets = Selector.SelectTarget(skillData, this.transform);
        }
        /// <summary>
        /// 影响效果
        /// </summary>
        public void ImpactTargets()
        {
            foreach (var item in Impacts)
            {
                item.Execute(this);
            }
        }
        /// <summary>
        /// 释放技能
        /// </summary>
        public virtual void DeploySkill()
        {
            SkillData.AttackedTargets.Clear();
        }
        /// <summary>
        /// 回收技能
        /// </summary>
        public abstract void RecoverGameObject();
    }
}

# 封装技能系统

  • 封装技能系统,对外提供简单的技能释放功能
  • 两种情况:为玩家使用、为 NPC 使用
  • 判断技能连击
  • 处理特殊情况:在一个技能释放的过程中释放其他技能
    • MOBA、拳皇等游戏会使用队列(或列表)
      • 在使用技能时入队,技能释放完成(调用动画事件)后出队
      • 如果需要记录技能的使用数据(例如:技能的释放顺序或技能的使用次数),可以使用列表
    • ARPG 游戏使用忽略操作(释放技能的过程中,无视其他指令)
      • 实现方法:判断动画的播放状态
  • 需要变换组件助手类

抽象类

CharacterSkillSystem
using Common;
using UnityEngine;
namespace SkillSystem
{
    [RequireComponent(typeof(CharacterSkillManager))]
    /// <summary>
    /// 封装技能系统,对外提供简单的技能释放功能
    /// </summary>
    public abstract class CharacterSkillSystem : MonoBehaviour
    {
        protected CharacterSkillManager SkillManager;
        protected AnimationEventBehaviour EventBehaviour;
        /// <summary>
        ///true: 技能成功释放
        /// </summary>
        protected bool SuccessfullyUsed;
        /// <summary>
        /// 最后一次释放的技能的数据
        /// </summary>
        private SkillData LastSkill;
        private Animator Anim;
        // 处理特殊情况:在一个技能释放的过程中释放其他技能
        //MOBA、拳皇等游戏会使用队列(或列表)
        //    在使用技能时入队,技能释放完成(调用动画事件)后出队
        //    如果需要记录技能的使用数据(例如:技能的释放顺序或技能的使用次数),可以使用列表
        //    private Queue<SkillData> SkillID;
        //ARPG 游戏使用忽略操作(释放技能的过程中,无视其他指令)
        //    实现方法:判断动画的播放状态
        protected virtual void Awake()
        {
            SkillManager = this.GetComponent<CharacterSkillManager>();
            EventBehaviour = this.GetComponentInChildren<AnimationEventBehaviour>();
            Anim = this.GetComponentInChildren<Animator>();
        }
        protected virtual void Start()
        {
            SuccessfullyUsed = false;
            EventBehaviour.onMeleeAttack.AddListener(DeploySkill);
            EventBehaviour.onRangedAttack.AddListener(DeploySkill);
        }
        /// <summary>
        /// 使用技能
        /// </summary>
        /// <param name="skillID"> 技能 ID</param>
        public virtual void UseSkill(int skillID)
        {
            // 是否连击
            if (LastSkill != null && LastSkill.IsBatter)
            {
                skillID = LastSkill.NextBatterID;
            }
            // 判断动画的播放状态
            if (LastSkill != null && Anim.GetBool(LastSkill.AnimationName))
            {
                SuccessfullyUsed = false;
                return;
            }
            // 准备技能
            LastSkill = SkillManager.PrepareSkill(skillID);
            if (LastSkill == null)
            {
                SuccessfullyUsed = false;
                return;
            }
            // 技能冷却
            SkillManager.StartCoroutine(SkillManager.CoolTimer(LastSkill));
            // 播放动画
            Anim.SetBool(LastSkill.AnimationName, true);
            // 单体攻击时朝向目标
            if (LastSkill.AttackType == SkillAttackType.Single)
            {
                // 查找目标
                LastSkill.AttackTargets = FindTargets();
                this.transform.LookAtTarget(LastSkill.AttackTargets?[0]);
            }
            SuccessfullyUsed = true;
        }
        /// <summary>
        /// 生成技能
        /// </summary>
        protected virtual void DeploySkill()
        {
            SkillManager.GenerateSkill(LastSkill, LastSkill.StartPosition, LastSkill.StartRotation);
        }
        /// <summary>
        /// 设置释放位置
        /// </summary>
        /// <param name="skillData"> 技能数据 & lt;/param>
        /// <param name="startPosition"> 起始位置 & lt;/param>
        /// <param name="startRotation"> 起始旋转 & lt;/param>
        public void SetPosition(Vector3 startPosition, Quaternion startRotation)
        {
            if (LastSkill == null)
            {
                return;
            }
            LastSkill.StartPosition = startPosition;
            LastSkill.StartRotation = startRotation;
        }
        /// <summary>
        /// 寻找目标
        /// </summary>
        /// <returns></returns>
        private Transform[] FindTargets()
        {
            // 这部分的代码与技能生成器中的代码一样,而且这里的代码一定优先执行,但是这两部分的代码一个都不能删
            // 虽然代码有重复,但是可以应对某些特殊情况:
            // 按下技能键时攻击范围内没有目标,但是动画播放到某一帧并释放技能的时候,
            // 攻击范围内有目标了,所以技能生成器中的代码是有必要的
            IAttackSelector selector = DeployerFactory.CreateAttackSelector(LastSkill); // 攻击选区
            Transform[] targets = selector.SelectTarget(LastSkill, this.transform);
            return targets == null || targets.Length == 0 ? null : targets;
        }
    }
}

为玩家使用

CharacterSkillSystemPlayer
using Character;
using UnityEngine;
namespace SkillSystem
{
    [RequireComponent(typeof(CharacterInputController), typeof(CharacterSelected))]
    /// <summary>
    /// 为玩家提供的技能系统
    /// </summary>
    public class CharacterSkillSystemPlayer : CharacterSkillSystem
    {
        private CharacterSelected SelectCharacter;
        private CharacterInputController InputController;
        protected override void Awake()
        {
            base.Awake();
            SelectCharacter = this.GetComponent<CharacterSelected>();
            InputController = GetComponent<CharacterInputController>();
        }
        protected override void Start()
        {
            base.Start();
            EventBehaviour.CancelAnim.AddListener(CanMove);
        }
        public override void UseSkill(int skillID, bool isBatter = false)
        {
            base.UseSkill(skillID, isBatter);
            if (SuccessfullyUsed)
            {
                // 禁止移动(攻击的优先级高于移动)
                InputController.IsMove = false;
                // 选中目标
                SelectCharacter.SelectTargets(Skill.AttackTargets);
            }
        }
        /// <summary>
        /// 使角色可以移动
        /// </summary>
        private void CanMove()
        {
            InputController.IsMove = true;
        }
    }
}

为 NPC 使用

CharacterSkillSystemNPC
using Common;
using UnityEngine;
namespace SkillSystem
{
    /// <summary>
    /// 为 NPC 提供的技能系统
    /// </summary>
    public class CharacterSkillSystemNPC : CharacterSkillSystem
    {
        /// <summary>
        /// 获取随机技能
        /// </summary>
        /// <returns></returns>
        public virtual SkillData GetRandomSkillData()
        {
            // 先选出可以生成的技能,然后在生成随机数
            SkillData[] skillData = SkillManager.Skills.FindAll(s => SkillManager.PrepareSkill(s.SkillID) != null);
            if (skillData != null && skillData.Length > 0)
            {
                int index = Random.Range(0, skillData.Length);
                return skillData[index];
            }
            return null;
        }
    }
}

# 使用方法

# 连接 UI

代码如下

using SkillSystem;
using Common;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace UI
{
    /// <summary>
    /// UIPlayCanvas
    /// </summary>
    public class UIPlayCanvas : UIWindow
    {
        [Tooltip("角色的技能系统")]
        public CharacterSkillSystem SkillSystem;
        private Button[] SkillButtons;
        private float LastBatterClickTime; // 上一次 按下 连击技能按钮 的 时间
        private void Start()
        {
            InitializeSkillButton();
            RegistrationEvent();
        }
        /// <summary>
        /// 初始化技能按键
        /// </summary>
        private void InitializeSkillButton()
        {
            // 找到所有技能按键
            Transform skillButton = TransformHelper.FindChildByName(this.transform, "SkillButtons");
            SkillButtons = new Button[skillButton.childCount];
            for (int i = 0; i < skillButton.childCount; i++)
            {
                SkillButtons[i] = skillButton.GetChild(i).GetComponent<Button>();
            }
        }
        /// <summary>
        /// 注册事件
        /// </summary>
        private void RegistrationEvent()
        {
            foreach (Button button in SkillButtons)
            {
                if (button.name == "1001")
                {
                    GetUIEventListener(button.name).PointerPress.AddListener(OnSkillButtonPress);
                }
                else
                {
                    GetUIEventListener(button.name).PointerClick.AddListener(OnSkillButtonClick);
                }
            }
        }
        /// <summary>
        /// OnSkillButtonPress
        /// </summary>
        /// <param name="eventData"></param>
        private void OnSkillButtonPress(PointerEventData eventData)
        {
            if (SkillSystem.Skill == null)
            {
                SkillSystem.UseSkill(int.Parse(eventData.pointerPress.name));
            }
            else
            {
                //SkillSystem.Skill 记录的是最后一次释放的技能的数据
                float interval = Time.time - LastBatterClickTime;
                if (interval < SkillSystem.Skill.BatterTimeMin) return;
                bool isBatter = interval <= SkillSystem.Skill.BatterTimeMax;
                SkillSystem.UseSkill(int.Parse(eventData.pointerPress.name), isBatter);
            }
            SetPosition();
            LastBatterClickTime = Time.time;
        }
        /// <summary>
        /// OnSkillButtonClick
        /// </summary>
        private void OnSkillButtonClick(PointerEventData eventData)
        {
            SkillSystem.UseSkill(int.Parse(eventData.pointerPress.name));
            SetPosition();
        }
        /// <summary>
        /// 设置释放位置
        /// </summary>
        private void SetPosition()
        {
            if (SkillSystem.Skill != null)
            {
                Transform owner = SkillSystem.Skill.Owner.transform;
                SkillSystem.SetPosition(owner.position, owner.rotation);
            }
        }
    }
}

# 接口的实现类 和 技能释放器的子类

  • 这些类需要根据具体的需求而定,这里给出一些例子
  • 技能移动的接口由技能释放器的子类来实现

# 扇形选区

代码如下

SectorAttackSelector
using Character;
using Common;
using System.Collections.Generic;
using UnityEngine;
namespace SkillSystem
{
    /// <summary>
    /// 扇形 / 圆形选区
    /// </summary>
    public class SectorAttackSelector : IAttackSelector
    {
        /// <summary>
        /// 获取目标
        /// </summary>
        /// <param name="skillData"> 技能数据 & lt;/param>
        /// <param name="skillTransform"> 技能位置 & lt;/param>
        /// <returns></returns>
        public Transform[] SelectTarget(SkillData skillData, Transform skillTransform)
        {
            // 指定范围搜索
            List<Transform> targets = skillTransform.SelectTargets(skillData.AttackDistance, skillData.AttackAngle, skillData.AttackTargetTags);
            // 选取活动的目标
            targets = targets.FindAll(t => t.GetComponent<CharacterStatus>().HP > 0);
            // 返回目标(群攻?单攻?)
            if (skillData.AttackType == SkillAttackType.Group)
            {
                return targets.ToArray();
            }
            else if (skillData.AttackType == SkillAttackType.Single)
            {
                Transform min = targets.ToArray().GetMin(t => Vector3.Distance(t.position, skillTransform.position));
                if (min == null)
                {
                    return null;
                }
                return new Transform[] { min };
            }
            return null;
        }
    }
}

# 消耗法力值

代码如下

using Character;
namespace SkillSystem
{
    /// <summary>
    /// 消耗法力值
    /// </summary>
    public class CostMPImpact : IImpactEffect
    {
        public void Execute(SkillDeployer skillDeployer)
        {
            CharacterStatus status = skillDeployer.SkillData.Owner.GetComponent<CharacterStatus>();
            status.MP -= skillDeployer.SkillData.CostMP;
        }
    }
}

# 伤害效果

  • 记录被攻击单位和受击次数

代码如下

DamageImpact
using Character;
using System.Collections;
using UnityEngine;
namespace SkillSystem
{
    /// <summary>
    /// 伤害效果
    /// </summary>
    public class DamageImpact : IImpactEffect
    {
        public void Execute(SkillDeployer skillDeployer)
        {
            SkillData skillData = skillDeployer.SkillData;
            // 如果写在 OnceDamage 中,每次调用方法都会执行一次 GetComponent
            // 如果写在这里,只需要执行一次 GetComponent
            CharacterStatus attackerStatus = skillData.Owner.GetComponent<CharacterStatus>();
            if (skillData.AttackTargets == null) return;
            if (skillData.DurationTime == 0)
            {
                OnceDamage(skillData, attackerStatus);
            }
            else
            {
                skillDeployer.StartCoroutine(RepeatDamage(skillDeployer, attackerStatus));
            }
        }
        /// <summary>
        /// 单次攻击
        /// 伤害 = 技能固定伤害 + 攻击力 * 技能倍率
        /// </summary>
        /// <param name="skillData"></param>
        /// <param name="attackerStatus"></param>
        private void OnceDamage(SkillData skillData, CharacterStatus attackerStatus)
        {
            float damage = skillData.AttackDamage + attackerStatus.AttackPower * skillData.AttackRatio;
            foreach (var item in skillData.AttackTargets)
            {
                item.GetComponent<CharacterStatus>().Damage(damage);
            }
            RecordAttack(skillData);
        }
        /// <summary>
        /// 持续性伤害效果
        /// </summary>
        /// <param name="skillDeployer"></param>
        /// <param name="attackerStatus"></param>
        /// <returns></returns>
        private IEnumerator RepeatDamage(SkillDeployer skillDeployer, CharacterStatus attackerStatus)
        {
            SkillData skillData = skillDeployer.SkillData;
            float durationTime = 0;
            do
            {
                OnceDamage(skillData, attackerStatus);
                yield return new WaitForSeconds(skillData.AttackInterval);
                durationTime += skillData.AttackInterval;
                skillDeployer.CalculateTargets(); // 重新计算目标
            }
            while (durationTime < skillData.DurationTime);
        }
        /// <summary>
        /// 记录被攻击单位和受击次数
        /// </summary>
        private void RecordAttack(SkillData skillData)
        {
            foreach (var item in skillData.AttackTargets)
            {
                if (!skillData.AttackedTargets.ContainsKey(item.name))
                {
                    skillData.AttackedTargets.Add(item.name, new SkillAttackedTarget(item, 1));
                }
                else
                {
                    skillData.AttackedTargets[item.name].Times++;
                }
            }
        }
        // 创建攻击特效(因为没有做特效,所以没写这部分的代码)
    }
}

# 近战技能释放器

代码如下

MeleeAttackSkillDeployer
using Common;
namespace SkillSystem
{
    /// <summary>
    /// 近战技能释放器
    /// </summary>
    public class MeleeAttackSkillDeployer : SkillDeployer
    {
        public override void DeploySkill()
        {
            base.DeploySkill();
            // 指定时间后回收目标
            RecoverGameObject();
            // 执行选区(寻找目标)算法
            CalculateTargets();
            // 执行影响算法
            ImpactTargets();
        }
        public override void RecoverGameObject()
        {
            ObjectPool.Instance.RecoverGameObject(this.gameObject, SkillData.DurationTime);
        }
    }
}

# 远程技能释放器

  • 释放投掷类型的技能
  • 一个目标只会受到一次伤害
  • 技能需要移动,所以继承接口 IMovable

代码如下

RemoteAttackSkillDeployer
using Common;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace SkillSystem
{
    /// <summary>
    /// 远程技能释放器
    /// </summary>
    public class RangedAttackSkillDeployer : SkillDeployer
    {
        private List<Transform> AllImpactTargets;
        private Vector3 TargetPosition;
        public override void DeploySkill()
        {
            base.DeploySkill();
            AllImpactTargets = new List<Transform>();
            SetSkillData();
            RecoverGameObject();
            MoveToTarget();
        }
        /// <summary>
        /// 设置数据
        /// </summary>
        /// <returns></returns>
        private void SetSkillData()
        {
            TargetPosition = SkillData.Owner.transform.TransformPoint(0, 0, SkillData.MoveDistance);
            SkillData.AttackedTargets.Clear();
        }
        public override void RecoverGameObject()
        {
            this.StartCoroutine(ToRecover());
        }
        /// <summary>
        /// 回收物体
        /// </summary>
        /// <returns></returns>
        private IEnumerator ToRecover()
        {
            while (Vector3.Distance(this.transform.position, TargetPosition) > 0.1f)
            {
                yield return null;
            }
            ObjectPool.Instance.RecoverGameObject(this.gameObject);
        }
        /// <summary>
        /// 移动到目标
        /// </summary>
        private void MoveToTarget()
        {
            this.StartCoroutine(ToMove());
        }
        /// <summary>
        /// 移动到目标
        /// </summary>
        private IEnumerator ToMove()
        {
            while (this.enabled)
            {
                this.transform.position = Vector3.MoveTowards(this.transform.position, TargetPosition, SkillData.MoveSpeed * Time.deltaTime);
                CalculateTargets();
                CalculateImpactTargets();
                ImpactTargets();
                yield return null;
            }
        }
        /// <summary>
        /// 计算受影响的目标
        /// 一个目标只会受到一次伤害
        /// </summary>
        private void CalculateImpactTargets()
        {
            if (SkillData.AttackTargets == null) return;
            AllImpactTargets.Clear();
            foreach (Transform item in SkillData.AttackTargets)
            {
                if (!SkillData.AttackedTargets.ContainsKey(item.name))
                {
                    AllImpactTargets.Add(item);
                }
            }
            SkillData.AttackTargets = AllImpactTargets.Count == 0 ? null : AllImpactTargets.ToArray();
        }
    }
}

# 类图

最后给出类图:
img

# 测试

代码部分已经完成了,接下来开始测试。

# 添加组件

组件和部分参数的设置

img
img
img
img
img
img

# 结果演示

img