【Unity】一个简易的技能系统(1)

这里针对2.5D的ARPG游戏做了一个简易的技能系统框架,技能系统可以说是一个游戏最核心的部分,涉及多个模块,耦合性高。

交互包括

对象间交互:施法者,受击者。
模块间交互:人物模块,NPC模块,场景管理模块,UI模块(技能cd),行走/寻路。
数据间交互:施法者攻防血数据,被击者攻防血数据。


技能分类:(设计抽象

按目标个数:单体、AOE(AOE
按攻击距离:近战、远程(远程
施法方式:瞬发、吟唱、还有引导,放大的时候不能动(如:烬)(都要考虑
伤害方式:一次瞬间结算、持续伤害、多次瞬间结算(船长的大)(一次瞬间结算、多次瞬间结算(最快也就是每帧结算)
目标对象:敌方(非己方)、己方、无差别
目标选择方式:点选、指定、范围(指定范围

基于以上内容给出技能系统设计框架图:

技能逻辑基类(SkillLogicBase)

技能逻辑基类是一个抽象类,每一个技能都是单独定制的,都是一个类,都需要继承技能逻辑基类。它包括每个技能都应该有的抽象部分,一个施法者和一个受击者,这里要注意放技能的不一定是主角,敌人AI的话也是可以放技能的,因此这里抽象出主角(Role)与敌人(Npc)的共同父类(Creature)。

public abstract class SkillLogicBase
{
    protected Creature _caster;//施法者
    protected Creature _target;//受击者
}

然后就是技能逻辑类中最核心,也是整个技能系统最核心的部分,技能时间线的运用。因为技能是有节奏的,是有时间先后顺序(如前摇,后摇等)。如我们放一个技能(远程飞行道具),刚开始道具不会立刻就飞出去,而是动作抬手到某个时间点才出去,所以引入时间概念,开始放技能最关键的就是时间线的启动。

技能时间线由两个类组成,一个是时间线的事件类(LineEvent),一个是技能时间线类(SkillTimeLine)。这个事件类是一个私有类,只会被SkillTimeLine所拥有,用于处理底层技能施放逻辑,而真正与SkillLogicBase交互的是SkillTimeLine。

private class LineEvent
    {

        public float Delay { get; protected set; }//延迟时间
        public int Id { get; protected set; }//操作的ID
        public Action<int> Method { get; protected set; }//回调函数
        public bool isInvoke = false;

        public LineEvent (float delay,int id,Action <int >method)
        {
            Delay = delay;
            Id = id;
            Method = method;
            Reset();//重置各种状态
        }

        public void Reset()
        {
            isInvoke = false;
        }

        //每帧执行(自己驱动的帧),time是从时间线开始,到目前为止经过的时间
        public void Invoke(float time)
        {
            //当前事件还没到延迟时间,直接返回
            if(time <Delay )
            {
                return;
            }          
            if (!isInvoke &&null !=Method )
            {
                isInvoke = true;
                Method(Id);//保证Method在时间线的整个生存周期内只会执行一次
            }
        }        
    }

针对SkillTimeLine而言,要处理的就是一系列事件的添加,最重要的是其中的AddEvent方法,有三个参数,第一个参数是延迟时间,到了这个时间点后开始调用第三个参数中的回调方法,第二个参数是int型参数,用于区分要做的事情,它是一个穿透参数(由调用者传进去的,中间这一部分仅仅只是作为一个保存,并且作为第三个参数的回调方法的参数,最终还要原封不动的返回给调用者)。

public class SkillTimeLine
{

    private bool _isStart;//是否开始
    private bool _isPause;//是否暂停
    private  float _curTime;//当前计时    
    private Action _reset;//重置事件
    private Action<float> _update;//每帧回调

    public SkillTimeLine ()
    {
        Reset();
    }

    /// <summary>
    /// 添加事件
    /// </summary>
    /// <param name="delay">延迟时间</param>
    /// <param name="id">ID(穿透参数)</param>
    /// <param name="method">执行的回调</param>
    public void AddEvent(float delay,int id,Action <int>method)
    {
        LineEvent param = new LineEvent(delay, id, method);
        _update += param.Invoke;//条件判断
        _reset += param.Reset;
    }

    //开始时间线
    public void Start()
    {
        Reset();
        _isStart = true;
        _isPause = false;
    }

    //重置(还原)
    public void Reset()
    {
        _curTime = 0;//时间线计时归零
        _isStart = false;//不开始
        _isPause = false;//没开始就不用谈暂停

        if(null !=_reset )
        {
            _reset();//在时间线的LineEvent里面去调用所有事件(Event)的reset函数,所有的时间线事件(LineEvent)也要归零
        }
    }

    public void Loop(float deltaTime)
    {
      
        if(!_isStart ||_isPause )//时间线开始并且没有被暂停就进入下面
        {
            return;
        }
        _curTime += deltaTime;
        if(null !=_update )
        {
            _update(_curTime);
        }
    }

技能时间线需要在技能逻辑中进行初始化,每个技能子类需要初始化的内容都不相同,因此该方法在父类中为抽象方法,子类必须实现。

protected SkillTimeLine _timeLine = new SkillTimeLine();//技能时间线

public void Init(Creature caster)
    {
        _caster = caster;
        //初始化时间线
        InitTimeLine();
    }
protected abstract void InitTimeLine();//每个技能的初始化都不同,父类中不用写时间线的初始化内容(为抽象方法)

当然SkillLogicBase中也包含各个技能子类公有的部分,包括技能的开始与结束,技能的动作开始与结束这四个。而这几个方法都是虚方法,支持在各个子类由于需求不同而进行定制。

//技能开始的时候穿透参数暂时不需要
    protected virtual void OnSkillStart(int __null)
    {
        //_skillCD.StartCD();//技能CD的初始化(暂时先不实现)
        Debug.Log("技能开始");
        //将施法者的朝向面向目标
        if (_target != null)
        {
            var realWatchPos = new Vector3(_target.transform.position.x, _caster.transform.position.y, _target.transform.position.z);
            _caster.transform.LookAt(realWatchPos);
        }
    }

    protected virtual void OnSkillEnd(int __null)//用虚方法,担心后面某些技能的逻辑比较复杂,                                                
    {
        Debug.Log("技能结束");
        //暂时就是先清空目标
        _target = null;

        //通知外界技能结束
        if (_skillEndCallback != null)
        {
            _skillEndCallback();
        }
    }

    //播放结束动画
    protected virtual void OnActionEnd(int actionID)
    {
        Debug.Log("动画结束");
        //先判断施放者当前动画是否与要播放的动画相同,相同才播放动画0(因为有可能被打断)
        if (_caster.GetAnim() == actionID)//这里要获取当前施法者的当前动画参数,因此还要在Creature中封装对应方法
        {
            _caster.SetAni(1);
        }
    }

    //播放相应动画
    protected virtual void OnAction(int actionID)
    {
        Debug.Log("动画开始");
        _caster.SetAni(actionID);
    }

至此SkillLogicBase中抽象出来各个技能子类所共有的部分,然后就是开始施放技能的逻辑,本质上就是技能时间线的开启以及驱动。由于技能结束后我们想做一些事情(如什么时候能够再次施放技能,因此这里需要一个回调方法通知到外面),因此在SkillLogicBase初始化的时候也传入技能结束回调。

    protected Action _skillEndCallback;//技能结束回调

    // 开始施放技能
    public void Start(Creature target, Action skillEndCallback)
    {
        _target = target;
        _skillEndCallback = skillEndCallback;
        _timeLine.Start();//时间线的启用
    }

    //技能时间线的驱动
    public void Loop()
    {
        _timeLine.Loop(Time.deltaTime);
    }

通过SkillLogicBase中的Start方法调用,以及在上层将时间线进行驱动后,就能施放一个技能了。当然这里只是实现了技能逻辑抽象类,真正想放一个技能,则需要对该抽象类进行一个实现。这里我们来简单的做一个远程飞行道具技能进行实现

远程飞行技能(SkillFlyBallLogic)实现:

首先继承SkillLogicBase,并实现它的抽象方法。其中技能的开始结束以及技能动作的开始结束在基类中都已经有了基本的定义,如果有特殊的需求可以重写基类中的这几个方法,这里就以最基本的为主。

public class SkillFlyBallLogic:SkillLogicBase 
{
    protected override void InitTimeLine()
    {
        _timeLine.AddEvent(0, 0, OnSkillStart);//技能开始(0秒技能开始,且无需参数默认0)
        _timeLine.AddEvent(0, 22, OnAction);//播放动画(刚开始0秒时就要播放动画,动画参数为10)
        
        _timeLine.AddEvent(1f, 0, OnFlyObject);//生成飞行道具(1秒的时候生成,无需参数)
       
        _timeLine.AddEvent(2.333f, 22, OnActionEnd);//停止动画
        _timeLine.AddEvent(2.333f, 0, OnSkillEnd);//技能结束
    }
}

 其中生成飞行道具的时间要看对应角色的动作,具体模型生成飞行道具的时间并不同。并且针对飞行道具,不是直接生成然后飞出去就可以,里面涉及到飞行速度,攻击到目标后的属性(攻防血)变化、飞行道具的大小等问题,因此将飞行道具作为一个飞行结算物类。可以做指向性,也可以做无指向型,由于无指向型比较简单,生成后直接朝着角色正前方发射即可,因此我们做指向型的,必须选择一个目标然后朝目标放出飞行道具。

飞行道具类实现

先初始化飞行道具的所有参数,并在update方法中通过受击者与施法者的位置进行向量运算,确定飞行的方向,最后通过触发器实现实现回调,将受击后要做的事情在其他类中进行注册,即将逻辑与实现进行分离

public class FlyObject:MonoBehaviour 
{


    private Creature _target;
    private Creature _caster;//添加一个施法者
    //命中目标回调
    public Action<FlyObject,Creature> onHitTargetCallback;
    private Rigidbody _rig;
    private SphereCollider _collider;
    
    public float flySpeed;

    public float colliderRadius;//无需传入值,因为在外面初始化的时候会赋值

    /// <summary>
    /// 飞行道具初始化
    /// </summary>
    /// <param name="caster">施法者</param>
    /// <param name="target">受击者</param>
    /// <param name="speed">飞行速度</param>
    /// <param name="radius">飞行道具半径</param>
    /// <param name="hitCallback">打到目标的回调</param>
    public void Init(Creature caster,Creature target,float speed,float radius,Action <FlyObject ,Creature>hitCallback)
    {
        _caster = caster;
        _target  = target;
        flySpeed = speed;
        colliderRadius = radius;
        onHitTargetCallback = hitCallback;
      
        _rig = this.gameObject.AddComponent<Rigidbody>();
        _rig.useGravity = false;//取消使用重力

        //添加碰撞器
        _collider = this.gameObject.AddComponent<SphereCollider>();
        //设置碰撞器半径
        _collider.radius = colliderRadius;
        //设置为触发器
        _collider.isTrigger = true;
    }

    private void Update()
    {
        if(_target ==null )
        {
            return;
        }

        _rig.velocity = (target.transform.position - this.transform.position).normalized * flySpeed;//设置飞行道具的飞行朝向
    }

    private void OnTriggerEnter(Collider other)
    {
        if (_target == null)
        {
            return;
        }       
        var creature = other.GetComponent<Creature>();       
        if (creature != _target)
        {
            return;
        }
        if (onHitTargetCallback != null)
        {
            onHitTargetCallback(this, creature);//受到飞行道具攻击后的回调
        }
    } 
}

飞行道具制作完毕后就可以回到SkillFlyBallLogic中完成OnFlyObject方法。首先得有一个飞行道具,这里我们在Unity中用一个Sphere来代替,在一个空场景中创建一个Sphere并制作成预制体放到Resources文件夹下面,方便动态加载。

private void OnFlyObject(int flyObjID)
    {
        Debug.Log("生成飞行道具");
        //生成飞行道具
        var ballRes = Resources.Load<GameObject>("Sphere");
        var ball = GameObject.Instantiate(ballRes, _caster.transform);
        //添加飞行道具脚本
        var flyObject = ball.AddComponent<FlyObject>();
        //飞行道具参数初始化
        flyObject.Init(_caster, _target,5, 1, OnHitSomething);
    }
    //飞行道具命中敌方后所需要做的事情
    private void OnHitSomething(FlyObject flyObject, Creature target)
    {
        //命中敌人后将火球销毁
        GameObject.Destroy(flyObject.gameObject);
        //伤害结算(暂时先打日志)
        Debug.Log("对目标造成了伤害");
    }

到这里我们远程飞行技能制作完毕,然后要在角色类中实现放技能。暂时先用SkillLogicBase来进行测试。现在需要技能与角色交互,也就是角色点击按键,实现放技能。

人物抽象类Creature

首先在Creature中进行简单的测试,在Awake中初始化一个远程飞行技能,并在Update中驱动时间线。

public class Creature : MonoBehaviour
{

    private Animator _animator;
    private SkillLogicBase _skillLogicBase;
    public Creature target;//目标

    private void Awake()
    {
        _animator = this.GetComponent<Animator>();
        //远程飞行技能测试
        _skillLogicBase = new SkillFlyBallLogic();
        _skillLogicBase.Init(this);
    }

    private void Update()
    {      
        //技能时间线驱动  
        _skillLogicBase.Loop();
        if(Input .GetKeyDown(KeyCode.Space))//测试(按下空格键放出技能)
        {
            if(target ==null )
            {
                return;
            }
            _skillLogicBase.Start(target, null);
        }
    }
}

Unity场景测试

用胶囊体作为角色,并用cube作为角色的前方。同时为Player与Enemy添加Creature脚本与Animator组件,并将Enemy拖入到Player的Creature脚本的target处,即为技能目标赋值。这里只是测试,并不用动画播放,只需要按着技能时间线把日志打印出来即可。

 运行场景后测试效果如下,按下空格键后可以放出技能,首先Player转向Enemy,然后到达放飞行道具的时间点,放出了飞行道具,然后技能结束。并且我测试后再将Enemy拖到另外位置,在此按键放技能,也同样的先转身朝向Enemy,再放出技能。

 整个放技能的流程基本已经实现,我们再来看看Console面板中打印的日志是否正确。

 我们总共放了两次技能,从上面日志的打印可以看出正式施放技能的流程没有问题,技能时间线也是正确的。

总结:

当我们将SkillLogicBase抽象出来之后就可以发现,实现一个技能相对于来说是比较简单的,不过其中的技能CD还没有对接上去,这里我们放到下一节完成。而且大家也看到了做飞行技能还做了对应的飞行道具类,为何这么做?为了让我们技能逻辑结构更加清晰,以及将我们的逻辑与实现相分离,飞行道具有他的职责所在,同时也可以是一个通用的类,剥离了这个结构,在另外的技能实现方法中该类仍然可以对接进去。后面我会继续制作其他技能,包括其他模块的制作,思想都与与这个大同小异。

整个简单的技能逻辑施放的流程已实现,后面会继续实现其他功能。

  • 9
    点赞
  • 65
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值