Unity3D 实现简单的Buff系统

今天来考虑一下,想要实现一个buff系统需要什么功能。
能力不行,写的不好,请大家指正完善,谢谢~~

在我们接到一个需求的时候,往往需要先分析一下需求,确定我们要实现什么样的功能,大部分的情况下需求功能由策划提供,而你只需要考虑怎么实现就行了。不过今天特殊,没有策划,只能我们自己分析考虑了。
更据以往玩过的游戏,来看看buff系统都有什么功能:
1.计时,一个buff往往都会有存在时间,几秒,几分,甚至几天的存在
2.层级,有的buff只有一层,而有的buff可以拥有多层,效果叠加,有了层数,我们还需要考虑到,buff结束后是一次消除,还是逐层消除
3.次数,有的buff只会执行一次,而有的buff可以在一段时间内一直执行
4.间隔调用,有的buff比如加血,往往会1秒或两秒钟才会执行一次,其他时间是不会执行
5.行为操控,眩晕buff,击飞buff 等都是对玩家行为进行一种操控,夺取玩家控制权。

根据以上结果,我们需要制作的功能基本上就很明确了:
1.时间控制
2.层级控制
3.执行次数
5.位移控制等操作

下面就开始书写代码了
首先,我们先来实现一个配置类,这个类用来实现加载buff的配置表信息

[System.Serializable]
public class BuffBase
{
    /// 
    /// BuffID
    /// 
    public int BuffID;
    /// 
    /// Buff类型
    /// 
    public BuffType BuffType;
    /// 
    /// 执行此
    /// 
    public BuffCalculateType BuffCalculate = BuffCalculateType.Loop;
    /// 
    /// 叠加类型
    /// 
    public BuffOverlap BuffOverlap = BuffOverlap.StackedLayer;
    /// 
    /// 消除类型
    /// 
    public BuffShutDownType BuffShutDownType = BuffShutDownType.All;
    /// 
    /// 如果是堆叠层数,表示最大层数,如果是时间,表示最大时间
    /// 
    public int MaxLimit = 0;
    /// 
    /// 执行时间
    /// 
    public float Time = 0;
    /// 
    /// 间隔时间
    /// 
    public float CallFrequency = 1;
    /// 
    /// 执行数值 比如加血就是每次加多少
    /// 
    public float Num;
}

接下来是上面使用的几个枚举类型

/// 
/// buff类型
/// 
public enum BuffType
{
    /// 
    /// 恢复HP
    /// 
    AddHp,
    /// 
    /// 增加最大血量
    /// 
    AddMaxHp,
    /// 
    /// 减血
    /// 
    SubHp,
    /// 
    /// 减最大生命值
    /// 
    SubMaxHp,

    /// 
    /// 眩晕
    /// 
    AddVertigo,
    /// 
    /// 被击浮空
    /// 
    AddFloated,
    /// 
    /// 击退
    /// 
    AddRepel,
    /// 
    /// 冲刺
    /// 
    AddSprint,
    /// 
    /// 被击浮空
    /// 
    AddDamageFloated,
    /// 
    /// 添加忽略重力
    /// 
    AddIsIgnoreGravity,

}

/// 
/// 叠加类型
/// 
public enum BuffOverlap
{
    None,
    /// 
    /// 增加时间
    /// 
    StackedTime,
    /// 
    /// 堆叠层数
    /// 
    StackedLayer,
    /// 
    /// 重置时间
    /// 
    ResterTime,
}

/// 
/// 关闭类型
/// 
public enum BuffShutDownType
{
    /// 
    /// 关闭所有
    /// 
    All,
    /// 
    /// 单层关闭
    /// 
    Layer,
}

/// 
/// 执行类型
/// 
public enum BuffCalculateType
{
    /// 
    /// 一次
    /// 
    Once,
    /// 
    /// 每次
    /// 
    Loop,
}

BuffType枚举,是我们用来控制buff类型的,比如眩晕,比如增加HP,比如减少HP等等…
BuffOverlap枚举,就是叠加类型,上面我们说,有的buff是叠加层数,有的buff是重置时间,或者增加时间。
BuffShutDownType枚举,这个枚举的作用是控制倒计时结束后,应该是减一层,还是直接清空buff

然后我们创建一个BuffManager类,用来配置我们buff属性:


public class BuffManager : MonoBehaviour {

    private static BuffManager _instance;
    public static BuffManager Instance
    {
        get { return _instance; }
    }

    public List buffBase = new List();
}

因为我们还没有配置表,所以在检视面板中直接填写数据就行了
Unity3D 实现简单的Buff系统_第1张图片

buff数据有了,我们还需要提供buff的执行方法吧,就是我使用这个buff后这个buff要做什么,是我们需要知道的。
这就需要我们在写一个类,实现buff 执行中的功能。
buff使用用到角色身上的,所以我们写一个脚本ActorBuff 挂载到角色身上。

Unity3D 实现简单的Buff系统_第2张图片
这个类的最主要功能:
1.添加buff
2.执行buff
3.buff移除buff
我们先来看看实现:

public class ActorBuff : ActorCompent {
    [SerializeField]
    private List buffs = new List();

    public override void OnInit()
    {
        InitBuff();
    }

    void InitBuff()
    {

    }

    void Update()
    {

    }

    /// 
    /// 执行buff
    /// 
    void FixedUpdate()
    {
        for (int i = buffs.Count - 1; i >= 0; i--)
        {
            buffs[i].OnTick(Time.deltaTime);
            if (buffs[i].IsFinsh)
            {
                buffs[i].CloseBuff();
                buffs.Remove(buffs[i]);
            }
        }
    }

    /// 
    /// 添加buff
    /// 
    /// 
    public void AddBuff(BuffData buffData)
    {
        if (!buffs.Contains(buffData))
        {
            buffs.Add(buffData);
            buffData.StartBuff();
        }
    }

    /// 
    /// 移除buff
    /// 
    /// 
    public void RemoveBuff(int buffDataID)
    {
        BuffData bd = GetBuff(buffDataID);
        if(bd!=null)
            bd.CloseBuff();
    }

    /// 
    /// 移除buff
    /// 
    /// 
    public void RemoveBuff(BuffData buffData)
    {
        if (buffs.Contains(buffData))
        {
            buffData.CloseBuff();
        }
    }

    /// 
    /// 获取buff
    /// 
    /// 
    /// 
    public BuffData GetBuff(int buffDataID)
    {
        for (int i = 0; i < buffs.Count; i++)
        {
            if (buffs[i].buffDataID == buffDataID)
                return buffs[i];
        }
        return null;
    }

    /// 
    /// 获取buff
    /// 
    /// 
    /// 
    public BuffData GetBuffByBaseID(int buffBaseID)
    {
        for (int i = 0; i < buffs.Count; i++)
        {
            if (buffs[i].BuffID == buffBaseID)
                return buffs[i];
        }
        return null;
    }

    /// 
    /// 获取buff
    /// 
    /// 
    /// 
    public BuffData[] GetBuff(BuffType buffType)
    {
        List buffdatas = new List();
        for (int i = 0; i < buffs.Count; i++)
        {
            if (buffs[i].BuffType == buffType)
                buffdatas.Add(buffs[i]);
        }
        return buffdatas.ToArray();
    }

    /// 
    /// 执行buff
    /// 
    /// 
    public void DoBuff(int buffID)
    {
        BuffManager.Instance.DoBuff(Actor,buffID);
    }
}

正如上面所说,这个类拥有这几个方法:
FixedUpdate()这个方法中用来实现buff的执行,因为我写的buff有牵扯到位移,所以我放到FixedUpdate中了
DoBuff(int buffID) 这个方法是添加buff 的开端,他会调用BuffManager.Instance.DoBuff(Actor,buffID)方法,将自己和要添加buff发送过去,在BuffManager中解析完buff数据后会生成BuffData数据,然后调用 AddBuff(BuffData buffData)方法,将buff添加到buffs列表中
AddBuff(BuffData buffData)正如上面所说 BuffManager解析完buff数据后会生成BuffData 调用进行添加
RemoveBuff buff执行完毕后移除buff
RemoveBuff 同上
GetBuffByBaseID(int buffBaseID)GetBuff(int buffDataID) ,GetBuff(BuffType buffType)三个方法都是获取buffdata,不过所需数据不同,其中GetBuffByBaseID是根据BuffBase中的BuffID 获取配置表的,而GetBuff是根据BuffData类自己生成的ID来获取,GetBuff(BuffType buffType) 就是根据buff类型来获取BuffData

在上面的类型调用了BuffManager的DoBuff(Actor,buffID) 方法,我们没有实现,我们先写个空方法来占个坑。在BuffManager写入两个方法:

    /// 
    /// 执行buff
    /// 
    /// 
    /// 
    public void DoBuff(Actor actor,int buffID)
    {
        DoBuff(actor,GetBuffBase(buffID));
    }
    /// 
    /// 执行buff
    /// 
    /// 
    /// 
    public void DoBuff(Actor actor, BuffBase buff)
    {

    }

功能先不实现,现在我们需要着重的来看一下BuffData类的实现,这个类是执行中的buff,主要的buff功能均在此类中实现。

写这个类的时候比较蛋疼的是当初写的不知道怎么想的把BuffBase中的属性在此类中基本上都写了一遍,其实只要把BuffBase引用过来就行了,不过要过年了我先不该了,你们看的时候知道这个问题就行了。

因为这个BuffData的创建频率应该比较高,因为添加buff的时候就需要创建一个,使用完了就销毁,所以为了避免buffdata的重建,我们需要将失效buffdata存起来,后面在需要的时候重置里面的数据,再次使用。

[Serializable]
public class BuffData
{
        /// 
    /// 缓存栈
    /// 
    private static Stack poolCache = new Stack();
    /// 
    /// BuffData下一个ID
    /// 
    public static int buffIndex { get; private set; }

    /// 
    /// 构造方法
    /// 
    private BuffData() {
        buffDataID = buffIndex++;
    }

        /// 
    /// 创建BuffData
    /// 
    /// 
    public static BuffData Create()
    {
        if (poolCache.Count < 1)
            return new BuffData();
        BuffData buffData = poolCache.Pop();
        return buffData;
    }
    /// 
    /// 弹出
    /// 
    /// 
    private static BuffData Pop()
    {
        if (poolCache.Count < 1)
        {
            BuffData bd = new BuffData();
            return bd;
        }
        BuffData buffData = poolCache.Pop();
        return buffData;
    }
    /// 
    /// 压入
    /// 
    /// 
    private static void Push(BuffData buffData)
    {
        poolCache.Push(buffData);
    }
}

这样,我们在创建BuffData的时候通过BuffData.Create()方法创建,它会先去poolCache缓存池中去查看有没有空闲的BuffData,如果有,就取出来使用,没有就进行创建。

按照逻辑上来说,万物有始有终,所以我们需要在BuffData中添加两个方法,即开始方法StartBuff()和结束方法CloseBuff()

    /// 
    /// 开始Buff
    /// 
    public void StartBuff()
    {
    }

    /// 
    /// 关闭buff
    /// 
    public void CloseBuff()
    {
    }

但是别忘了,我们还需要一个中间不断执行的方法OnTick(float delayTime)

 /// 
 /// 执行中
 /// 
 public void OnTick(float delayTime)
 {

 }

这样,我们开始,执行中,结束方法都有了,感觉主题框架搭完了,我们得开始写实际功能了。
首先,我们需要先实现类的属性,另外,我们还想在buff执行时候发送一个事件,和结束的时候发送一个事件给我们,所以我们还需要定义几个事件

    /// 
    /// ID
    /// 
    public int buffDataID;
    /// 
    /// 配置表ID
    /// 
    public int BuffID;
    /// 
    /// buff类型
    /// 
    public BuffType BuffType;
    /// 
    /// 叠加类型
    /// 
    public BuffOverlap BuffOverlap = BuffOverlap.StackedLayer;
    /// 
    /// 执行次数
    /// 
    public BuffCalculateType BuffCalculate = BuffCalculateType.Loop;
    /// 
    /// 关闭类型
    /// 
    public BuffShutDownType BuffShutDownType = BuffShutDownType.All;
    /// 
    /// 最大限制
    /// 
    public int MaxLimit;
    /// 
    /// 当前数据
    /// 
    [SerializeField]
    private int _Limit;
    public int GetLimit { get { return _Limit; } }
    /// 
    /// 执行时间
    /// 
    [SerializeField]
    private float PersistentTime;
    public float GetPersistentTime { get { return PersistentTime; } }
    /// 
    /// 当前时间
    /// 
    [SerializeField]
    private float _CurTime;
    /// 
    /// 调用频率
    /// 
    public float CallFrequency { get; set; }
    /// 
    /// 当前时间
    /// 
    private float _curCallFrequency { get; set; }
    /// 
    /// 执行次数
    /// 
    [SerializeField]
    private int index = 0;

    /// 
    /// 开始调用
    /// 
    public Action OnStart;
    /// 
    /// 结束调用
    /// 
    public Action OnFinsh;

上面很多属性其实不需要重复的写,注意了直接引用BuffBase的实例即可,但是以下划线开头的属性,是需要在这个类中定义的,他们控制时间,当前数量等数据。
我们构造方法中最好重置一个部分属性的值,看起来向这样:

   /// 
    /// 构造方法
    /// 
    private BuffData() {
        buffDataID = buffIndex++;
        CallFrequency = 1;
        PersistentTime = 0;
    }

然后我们开始写开始方法:

    /// 
    /// 开始Buff
    /// 
    public void StartBuff()
    {
        //ChangeLimit(MaxLimit);
        _isFinsh = false;
        if (OnStart != null)
            OnStart();
    }
首先我们要确保_isFinsh = false,确保我们buff尚未结束,其次,就是发送事件了,通知我们buff要开始运行了。
其次,我们写结束方法:
/// 
/// 关闭buff
/// 
public void CloseBuff()
{
    if (OnFinsh != null)
        OnFinsh();
    Clear();
}
//重置数据
public void Clear()
{
    _Limit = 0;
    BuffID = -1;
    index = 0;
    PersistentTime = 0;
    _CurTime = 0;
    Data = null;
    CallFrequency = 0;
    _curCallFrequency = 0;
    //OnCallBackParam = null;
    //OnCallBack = null;
    OnStart = null;
    OnFinsh = null;
    _isFinsh = false;
    Push(this);
}

同样,发送结束事件,其次,调用Clear()方法重置部分属性值。最后别忘了Push(this),将此类插入缓存中。

另外,我们buff叠加方式,有所不同,所以我们想在一些特殊的情况下通知一个事件,所以我在BuffData又添加了以下几个事件:

    /// 
    /// 根据 CallFrequency 间隔 调用 结束时会调用一次 会传递 Data数据
    /// 
    public Action<object> OnCallBackParam;
    /// 
    ///   /// 
    /// 根据 CallFrequency 间隔 调用 结束时会调用一次 会传递 Data数据 int 次数
    /// 
    /// 
    public Action<object, int> OnCallBackParamIndex;
    /// 
    /// 根据 CallFrequency 间隔 调用 结束时会调用一次
    /// 
    public Action OnCallBack;
    /// 
    /// 根据 CallFrequency 间隔 调用 结束时会调用一次 int 次数
    /// 
    public Action<int> OnCallBackIndex;

    /// 
    /// 当改变时间
    /// 
    public Action OnChagneTime;
    /// 
    /// 当添加层
    /// 
    public Action OnAddLayer;
    /// 
    /// 当删除层
    /// 
    public Action OnSubLayer;

加载上面OnFinsh 事件下就行了。

剩下的就是需要实现OnTick方法了:

    /// 
    /// 执行中
    /// 
    /// 
    public void OnTick(float delayTime)
    {
        _CurTime += delayTime;
        //判断时间是否结束
        if (_CurTime >= PersistentTime)
        {
            ///调用事件
            CallBack();
            //判断结束类型 为层方式
            if (BuffShutDownType == BuffShutDownType.Layer)
            {
                SubLayer();
                //判读层数小于1 则结束
                if (_Limit <= 0)
                {
                    _isFinsh = true;
                    return;
                }
                //重置时间
                _curCallFrequency = 0;
                _CurTime = 0;
                return;
            }
            _isFinsh = true;
            return;
        }

        //如果是按频率调用
        if (CallFrequency > 0)
        {
            _curCallFrequency += delayTime;
            if (_curCallFrequency >= CallFrequency)
            {
                _curCallFrequency = 0;
                CallBack();
            }
            return;
        }
        ///调用回调
        CallBack();
    }

    /// 
    /// 调用回调
    /// 
    private void CallBack()
    {
        //次数增加
        index++;
        //判断buff执行次数 
        if (BuffCalculate == BuffCalculateType.Once)
        {
            if (index > 1) { index = 2; return; }
        }

        if (OnCallBack != null)
            OnCallBack();
        if (OnCallBackIndex != null)
            OnCallBackIndex(index);
        if (OnCallBackParam != null)
            OnCallBackParam(Data);
        if (OnCallBackParamIndex != null)
            OnCallBackParamIndex(Data, index);
    }

    /// 
    /// 加一层
    /// 
    public void AddLayer()
    {
        _Limit++;
        _CurTime = 0;
        if (_Limit > MaxLimit)
        {
            _Limit = MaxLimit;
            return;
        }
        if (OnAddLayer != null)
            OnAddLayer(this);
    }

    /// 
    /// 减一层
    /// 
    public void SubLayer()
    {
        _Limit--;
        if (OnSubLayer != null)
            OnSubLayer(this);
    }

    /// 
    /// 重置时间
    /// 
    public void ResterTime()
    {
        _CurTime = 0;
    }

    /// 
    /// 修改 时间
    /// 
    /// 
    public void ChangePersistentTime(float time)
    {
        PersistentTime = time;
        if (PersistentTime >= MaxLimit)
            PersistentTime = MaxLimit;
        if (OnChagneTime != null)
            OnChagneTime(this);
    }

主要流程控制是在Tick方法中执行的。而主要的逻辑操作是在提供的几个事件中进行编辑。这个先不急,我们再在BuffData中添加几个方法:

    /// 
    /// 获取当前执行时间
    /// 
    public float GetCurTime
    {
        get { return _CurTime; }
    }
    /// 
    /// 是否结束
    /// 
    public bool IsFinsh
    {
        get { return _isFinsh; }
    }

根据需求,我们在写几个创建方法,和构造函数:


    private BuffData(float persistentTime,Action onCallBack)
    {

        PersistentTime = persistentTime;
        OnCallBack = onCallBack;
        buffDataID = buffIndex++;
    }

    public static BuffData Create(BuffBase buffBase,Action onCallBack)
    {
       return Create(buffBase,onCallBack,null,null);
    }

    public static BuffData Create(BuffBase buffBase, Action onCallBack,Action addLayerAcion,Action subLayerAction)
    {
        BuffData db = Pop();
        db.BuffCalculate = buffBase.BuffCalculate;
        db.BuffID = buffBase.BuffID;
        db.CallFrequency = buffBase.CallFrequency;
        db.PersistentTime = buffBase.Time;
        db.BuffOverlap = buffBase.BuffOverlap;
        db.BuffShutDownType = buffBase.BuffShutDownType;
        db.BuffType = buffBase.BuffType;
        db.MaxLimit = buffBase.MaxLimit;
        db.OnCallBack = onCallBack;
        db.OnAddLayer = addLayerAcion;
        db.OnSubLayer = subLayerAction;
        db._Limit = 1;
        return db;
    }

以上只是更据需求添加就行了,不是必要的。
至此,我们BuffData列结束了。
接下来,Buff的流程控制有,我们还需要具体的执行方法,还记得上面提供的几个事件吗?在事件通知的时候我们进行逻辑操作就行了。
所以我们需要完善一下BuffManager类。
首先,我们在上面的BuffManager类中添加了一个方法DoBuff(Actor actor, BuffBase buff),如果忘记了可以翻上去看看。然后我们只实现了方法,还没实现功能,接下来我们就需要实现次数功能了。
它看起像这样:

 /// 
    /// 执行buff
    /// 
    /// 
    /// 
    public void DoBuff(Actor actor, BuffBase buff)
    {
        if (buff == null) return;
        BuffData db = null;

        switch (buff.BuffType)
        {
            case BuffType.AddHp: //增加血量
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, delegate
                    {
                        actor.ActorAttr.AddHP((int)buff.Num);
                    });

                }
                break;
            case BuffType.AddMaxHp: //增加最大血量
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, delegate
                    {
                        actor.ActorAttr.AddMaxHP((int)buff.Num);
                    }, delegate {
                        actor.ActorAttr.AddMaxHP((int)buff.Num);
                    }, delegate {
                        actor.ActorAttr.SubMaxHp((int)buff.Num);
                    });
                }
                break;
            case BuffType.SubHp: //减少血量
                if (!IsAdd(actor, buff))
                { 
                    db = BuffData.Create(buff, delegate
                    {
                        actor.ActorAttr.SubHp((int)buff.Num);
                    });
                }
                break;
            case BuffType.SubMaxHp: //减少最大血量
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, delegate
                    {
                        actor.ActorAttr.SubMaxHp((int)buff.Num);
                    }, delegate
                    {
                        actor.ActorAttr.SubMaxHp((int)buff.Num);
                    }, delegate
                    {
                        actor.ActorAttr.AddMaxHP((int)buff.Num);
                    });
                }
                break;
            case BuffType.AddDamageFloated: //浮空
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, delegate
                    {
                        if (actor.ActorState != ActorState.DamageRise)
                            actor.ActorAttr.DamageRiseAbility = buff.Num;
                        actor.SetDamageRiseState();
                    });
                }
                break;
            case BuffType.AddFloated:
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, delegate
                    {
                        Vector3 moveDir = Vector3.up;
                        moveDir *= buff.Num;
                        actor.CharacterController.Move(moveDir*Time.deltaTime);
                    });
                }
                 break;
            case BuffType.AddSprint:
                if (!IsAdd(actor,buff))
                {
                    db = BuffData.Create(buff, delegate {
                        Vector3 moveDir = actor.transform.forward;
                        moveDir *= buff.Num;
                        moveDir.y += -20;
                        actor.CharacterController.Move(moveDir*Time.deltaTime);
                        //actor.Translate(Vector3.forward * buff.Num * Time.deltaTime);
                    });
                }
                break;
            case BuffType.AddIsIgnoreGravity:
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, null);
                    db.OnStart = delegate { actor.ActorPhysical.IsIgnoreGravity = true; };
                    db.OnFinsh = delegate
                    {
                        actor.ActorPhysical.IsIgnoreGravity = false;
                    };
                }
                break;
        }
        if (db != null)
            actor.ActorBuff.AddBuff(db);
    }

    /// 
    /// 玩家是否已经有此buff
    /// 
    /// 
    /// 
    /// 
    private bool IsAdd(Actor actor,BuffBase buff)
    {
        BuffData oldBuff = actor.ActorBuff.GetBuffByBaseID(buff.BuffID);
        if (oldBuff != null)
        {
            switch (buff.BuffOverlap)
            {
                case BuffOverlap.ResterTime:
                    oldBuff.ResterTime();
                    break;
                case BuffOverlap.StackedLayer:
                    oldBuff.AddLayer();
                    break;
                case BuffOverlap.StackedTime:
                    oldBuff.ChangePersistentTime(oldBuff.GetPersistentTime + buff.Time);
                    break;
                default:
                    break;
            }
            return true;
        }
        return false;
    }

    /// 
    /// 获取配置数据
    /// 
    /// 
    /// 
    public BuffBase GetBuffBase(int buffID)
    {
        for (int i = 0; i < buffBase.Count; i++)
        {
            if (buffBase[i].BuffID == buffID)
                return buffBase[i];
        }
        return null;
    }

注意,后面还跟了几个方法,我们先来介绍一下:
IsAdd(Actor actor,BuffBase buff)方法,主要是查看玩家身上是否已经曾在相同的buff,并且根据BuffOverlap(叠加类型)进行相对应的buff叠加。还记得在BuffData中添加的那几个方法吗?ResterTime()AddLayer()ChangePersistentTime() 分别对应重置时间,叠加层、修改时间,注意哟我这里叠加层会重置buff时间,当然你也可以不用重置时间。

GetBuffBase(int buffID)这个方法作用,就是获取配置表,当然现在我们没用配置表,所以直接检索列表就行。

最后我们介绍DoBuff(Actor actor, BuffBase buff)方法中内容:
根据上面的代码来看一大串的case,这个显然不好看,但是你们知道的,过年了。不过这个地方还是可以用工厂或者注册类的方式去做,我这里直接用switch和case了。
我们先看BuffType.AddHp的分支:
我们先看一下addhp的配置
Unity3D 实现简单的Buff系统_第3张图片
id为0,buff类型为Addhp(BuffType) ,添加血量,执行次数(BuffCalculate)为Loop就是不断的执行,叠加方式(BuffOverlap) 时间叠加,清除类型(Buff Shut Down)为层清除,在这里这个清除类型什么样的都可以。根据上下文知道,添加血量按时间计算最大限制就是1000秒(MaxLimit),持续时间为5秒(Time),调用频率为1秒一次(CallFrequency)。每次增加血量为10(num).
那我们在添加 Buff的时候就会调用BuffManager中的DoBuff方法,通过BuffType执行BuffType.AddHpcase分支中的逻辑:
首先,我们需要判断一下玩家身上是否存在该类型buff,没错就是通过IsAdd方法,如果有,则根据叠加方式进行叠加,并返回true,否则返回flase。
其次,如果为flase,那代表我们要创建BuffData类了,这个是什么呢?就是运行时的Buff类。我们调用Create(BuffBase buffBase,Action onCallBack),两个参数,一个是buff数据,一个回调方法,因为AddHP的Call Frequency为1,所以会走频率调用的方,就是BuffData中的Tick方法:

  //如果是按频率调用
        if (CallFrequency > 0)
        {
            _curCallFrequency += delayTime;
            if (_curCallFrequency >= CallFrequency)
            {
                _curCallFrequency = 0;
                CallBack();
            }
            return;
        }

这样这里CallBack就会按频率进行调用。

db = BuffData.Create(buff, delegate
{
      actor.ActorAttr.AddHP((int)buff.Num);
});

CallBack里的逻辑,就是你增加血量的逻辑了。如上,这是我的加血方法,你应该提供你自己的方法

最后别忘了把BuffData添加到玩家身上进行执行:

if (db != null)
      actor.ActorBuff.AddBuff(db);

这样我们一个加血的buff制作完成了。
根据上面,我在研究一个AddMaxHp的Buff,一看就很明白,这是一个临时增加最大HP的buff,同样我们先看配置:
Unity3D 实现简单的Buff系统_第4张图片
与上面AddHP不同的是,这里我们调用次数为一次,叠加方式为层叠加,调用频率为0。
按照上面的流程,判断是否存在,存在,叠加,不存在创建。

 db = BuffData.Create(buff, delegate
 {
        actor.ActorAttr.AddMaxHP((int)buff.Num);
    }, delegate {
        actor.ActorAttr.AddMaxHP((int)buff.Num);
    }, delegate {
        actor.ActorAttr.SubMaxHp((int)buff.Num);
 });

调用的是Create(BuffBase buffBase, Action onCallBack,Action addLayerAcion,Action subLayerAction) 方法。
相比上面而言,多了addLayerAcionsubLayerAction两个事件,根据BuffData中的代码知道addLayerAcion是在添加层是调用,而subLayerAction是在删除层时调用。
而onCallBack是在第一次运行该buff的时候调用一次。
我们来看一下,我默认的血量
Unity3D 实现简单的Buff系统_第5张图片
然后我开始执行buff,
Unity3D 实现简单的Buff系统_第6张图片
我执行3次,根据配置,知道一次叠加增加100点血量。那三次后应该是1300的血量
Unity3D 实现简单的Buff系统_第7张图片
正确,为了方便查看buff,我把BuffData在检视面板中显示出来了,我们来看看ActorBuff中的内容
Unity3D 实现简单的Buff系统_第8张图片
我们可以看到,当前的MaxLimit 为10,Limit为3 ,Persistent Time 为10。在这里MaxLimit表示最大层数,也就说最大血量叠加最多只能叠加10层,如果超过10层则强制为0层,Limit为当前层数,PersistentTime 为每层存在的时间。
通过实例,就可以看到,没过10秒钟,就会减少一层知道所有结束为止,血量也会变回原来的数值。

上面的两个例子都是关于属性的增减,下面看一个关于控制的例子:
每个游戏基本都会有眩晕,但不幸的是,我没写。不过我这里有个关于忽略重力的例子和眩晕差不多:

 case BuffType.AddIsIgnoreGravity:
      if (!IsAdd(actor, buff))
        {
            db = BuffData.Create(buff, null);
            db.OnStart = delegate { actor.ActorPhysical.IsIgnoreGravity = true; };
            db.OnFinsh = delegate
            {
                actor.ActorPhysical.IsIgnoreGravity = false;
            };
        }
        break;

大概流程都差不多,不同的回调的使用方法。这里同样创建一个BuffData,但是使用的回调是OnStartOnFinsh
在回调用实现了忽略重力的方法。
在这里,配置表中的数据基本上没什么作用,除了一个时间,不过我这里没有用配置表里的时间,我用的动画时间。

最后一个,位移的例子:
比如我有个一个技能,我想让英雄使用该技能时进行冲刺,就是一个直线位移,通过buff怎么实现呢?

 case BuffType.AddSprint:
      if (!IsAdd(actor,buff))
       {
           db = BuffData.Create(buff, delegate {
               Vector3 moveDir = actor.transform.forward;
               moveDir *= buff.Num;
               moveDir.y += -20;
               actor.CharacterController.Move(moveDir*Time.deltaTime);
               //actor.Translate(Vector3.forward * buff.Num * Time.deltaTime);
           });
       }
       break;

上面这个buff我就进行简单位移操作。
我们看一下配置
Unity3D 实现简单的Buff系统_第9张图片
这个buff中我们需要用到3个属性:执行次数为Loop,持续时间,距离。
注意要将CallFrequency设置为零这样才能保证CallBack每帧都会调用。
然后在回调中实现位移方法就OK了

为了防止我书写的漏掉什么代码,下面我把完整的代码贴出来:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class BuffManager : MonoBehaviour {

    private static BuffManager _instance;
    public static BuffManager Instance
    {
        get { return _instance; }
    }

    public List buffBase = new List();


    #region GUI
    [SerializeField]
    private Actor TestActor;
    #endregion

    void Awake()
    {
        _instance = this;

        GUITools.ResterWindowAction(new Rect(300, 0, 220, 120), delegate(GUIAction action)
        {
            action.Rect = GUI.Window(action.Id, action.Rect, delegate
            {
                action.Param[0] = GUI.TextField(new Rect(10, 20, 200, 20), action.Param[0]);
                action.Param[1] = GUI.TextField(new Rect(10, 45, 200, 20), action.Param[1]);
                if (GUI.Button(new Rect(10, 70, 200, 30), "AddBuff"))
                {
                    if (TestActor == null || !TestActor.gameObject.Equals(action.Param[1]))
                    {
                        GameObject obj = GameObject.Find(action.Param[1]);
                        if (obj != null)
                            TestActor = obj.GetComponent();
                    }
                    if (TestActor != null)
                        DoBuff(TestActor, int.Parse(action.Param[0]));
                    else
                        GUI.Label(new Rect(10, 105, 200, 10), "测试Actor 为 null ,请检查Actor Name是否正确!");
                }
                GUI.DragWindow();
            }, action.Id+ " - Buff Test|BuffManager");

        },"0","Player");
    }

    /// 
    /// 执行buff
    /// 
    /// 
    /// 
    public void DoBuff(Actor actor,int buffID)
    {
        DoBuff(actor,GetBuffBase(buffID));
    }

    /// 
    /// 执行buff
    /// 
    /// 
    /// 
    public void DoBuff(Actor actor, BuffBase buff)
    {
        if (buff == null) return;
        BuffData db = null;

        switch (buff.BuffType)
        {
            case BuffType.AddHp: //增加血量
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, delegate
                    {
                        actor.ActorAttr.AddHP((int)buff.Num);
                    });

                }
                break;
            case BuffType.AddMaxHp: //增加最大血量
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, delegate
                    {
                        actor.ActorAttr.AddMaxHP((int)buff.Num);
                    }, delegate {
                        actor.ActorAttr.AddMaxHP((int)buff.Num);
                    }, delegate {
                        actor.ActorAttr.SubMaxHp((int)buff.Num);
                    });
                }
                break;
            case BuffType.SubHp: //减少血量
                if (!IsAdd(actor, buff))
                { 
                    db = BuffData.Create(buff, delegate
                    {
                        actor.ActorAttr.SubHp((int)buff.Num);
                    });
                }
                break;
            case BuffType.SubMaxHp: //减少最大血量
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, delegate
                    {
                        actor.ActorAttr.SubMaxHp((int)buff.Num);
                    }, delegate
                    {
                        actor.ActorAttr.SubMaxHp((int)buff.Num);
                    }, delegate
                    {
                        actor.ActorAttr.AddMaxHP((int)buff.Num);
                    });
                }
                break;
            case BuffType.AddDamageFloated: //浮空
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, delegate
                    {
                        if (actor.ActorState != ActorState.DamageRise)
                            actor.ActorAttr.DamageRiseAbility = buff.Num;
                        actor.SetDamageRiseState();
                    });
                }
                break;
            case BuffType.AddFloated:
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, delegate
                    {
                        Vector3 moveDir = Vector3.up;
                        moveDir *= buff.Num;
                        actor.CharacterController.Move(moveDir*Time.deltaTime);
                    });
                }
                 break;
            case BuffType.AddSprint:
                if (!IsAdd(actor,buff))
                {
                    db = BuffData.Create(buff, delegate {
                        Vector3 moveDir = actor.transform.forward;
                        moveDir *= buff.Num;
                        moveDir.y += -20;
                        actor.CharacterController.Move(moveDir*Time.deltaTime);
                        //actor.Translate(Vector3.forward * buff.Num * Time.deltaTime);
                    });
                }
                break;
            case BuffType.AddIsIgnoreGravity:
                if (!IsAdd(actor, buff))
                {
                    db = BuffData.Create(buff, null);
                    db.OnStart = delegate { actor.ActorPhysical.IsIgnoreGravity = true; };
                    db.OnFinsh = delegate
                    {
                        actor.ActorPhysical.IsIgnoreGravity = false;
                    };
                }
                break;
        }
        if (db != null)
            actor.ActorBuff.AddBuff(db);
    }

    /// 
    /// 玩家是否已经有此buff
    /// 
    /// 
    /// 
    /// 
    private bool IsAdd(Actor actor,BuffBase buff)
    {
        BuffData oldBuff = actor.ActorBuff.GetBuffByBaseID(buff.BuffID);
        if (oldBuff != null)
        {
            switch (buff.BuffOverlap)
            {
                case BuffOverlap.ResterTime:
                    oldBuff.ResterTime();
                    break;
                case BuffOverlap.StackedLayer:
                    oldBuff.AddLayer();
                    break;
                case BuffOverlap.StackedTime:
                    oldBuff.ChangePersistentTime(oldBuff.GetPersistentTime + buff.Time);
                    break;
                default:
                    break;
            }
            return true;
        }
        return false;
    }

    /// 
    /// 获取配置数据
    /// 
    /// 
    /// 
    public BuffBase GetBuffBase(int buffID)
    {
        for (int i = 0; i < buffBase.Count; i++)
        {
            if (buffBase[i].BuffID == buffID)
                return buffBase[i];
        }
        return null;
    }
}

/// 
/// buff类型
/// 
public enum BuffType
{
    /// 
    /// 恢复HP
    /// 
    AddHp,
    /// 
    /// 增加最大血量
    /// 
    AddMaxHp,
    /// 
    /// 减血
    /// 
    SubHp,
    /// 
    /// 减最大生命值
    /// 
    SubMaxHp,

    /// 
    /// 眩晕
    /// 
    AddVertigo,
    /// 
    /// 被击浮空
    /// 
    AddFloated,
    /// 
    /// 击退
    /// 
    AddRepel,
    /// 
    /// 冲刺
    /// 
    AddSprint,
    /// 
    /// 被击浮空
    /// 
    AddDamageFloated,
    /// 
    /// 添加忽略重力
    /// 
    AddIsIgnoreGravity,

}

/// 
/// 叠加类型
/// 
public enum BuffOverlap
{
    None,
    /// 
    /// 增加时间
    /// 
    StackedTime,
    /// 
    /// 堆叠层数
    /// 
    StackedLayer,
    /// 
    /// 重置时间
    /// 
    ResterTime,
}

/// 
/// 关闭类型
/// 
public enum BuffShutDownType
{
    /// 
    /// 关闭所有
    /// 
    All,
    /// 
    /// 单层关闭
    /// 
    Layer,
}

/// 
/// 执行类型
/// 
public enum BuffCalculateType
{
    /// 
    /// 一次
    /// 
    Once,
    /// 
    /// 每次
    /// 
    Loop,
}

[System.Serializable]
public class BuffBase
{
    /// 
    /// BuffID
    /// 
    public int BuffID;
    /// 
    /// Buff类型
    /// 
    public BuffType BuffType;
    /// 
    /// 执行此
    /// 
    public BuffCalculateType BuffCalculate = BuffCalculateType.Loop;
    /// 
    /// 叠加类型
    /// 
    public BuffOverlap BuffOverlap = BuffOverlap.StackedLayer;
    /// 
    /// 消除类型
    /// 
    public BuffShutDownType BuffShutDownType = BuffShutDownType.All;
    /// 
    /// 如果是堆叠层数,表示最大层数,如果是时间,表示最大时间
    /// 
    public int MaxLimit = 0;
    /// 
    /// 执行时间
    /// 
    public float Time = 0;
    /// 
    /// 间隔时间
    /// 
    public float CallFrequency = 1;
    /// 
    /// 执行数值 比如加血就是每次加多少
    /// 
    public float Num;
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System;

public class ActorBuff : ActorCompent {
    [SerializeField]
    private List buffs = new List();

    public override void OnInit()
    {
        InitBuff();
    }

    void InitBuff()
    {

    }

    void Update()
    {

    }

    /// 
    /// 执行buff
    /// 
    void FixedUpdate()
    {
        for (int i = buffs.Count - 1; i >= 0; i--)
        {
            buffs[i].OnTick(Time.deltaTime);
            if (buffs[i].IsFinsh)
            {
                buffs[i].CloseBuff();
                buffs.Remove(buffs[i]);
            }
        }
    }

    /// 
    /// 添加buff
    /// 
    /// 
    public void AddBuff(BuffData buffData)
    {
        if (!buffs.Contains(buffData))
        {
            buffs.Add(buffData);
            buffData.StartBuff();
        }
    }

    /// 
    /// 移除buff
    /// 
    /// 
    public void RemoveBuff(int buffDataID)
    {
        BuffData bd = GetBuff(buffDataID);
        if(bd!=null)
            bd.CloseBuff();
    }

    /// 
    /// 移除buff
    /// 
    /// 
    public void RemoveBuff(BuffData buffData)
    {
        if (buffs.Contains(buffData))
        {
            buffData.CloseBuff();
        }
    }

    /// 
    /// 获取buff
    /// 
    /// 
    /// 
    public BuffData GetBuff(int buffDataID)
    {
        for (int i = 0; i < buffs.Count; i++)
        {
            if (buffs[i].buffDataID == buffDataID)
                return buffs[i];
        }
        return null;
    }

    /// 
    /// 获取buff
    /// 
    /// 
    /// 
    public BuffData GetBuffByBaseID(int buffBaseID)
    {
        for (int i = 0; i < buffs.Count; i++)
        {
            if (buffs[i].BuffID == buffBaseID)
                return buffs[i];
        }
        return null;
    }

    /// 
    /// 获取buff
    /// 
    /// 
    /// 
    public BuffData[] GetBuff(BuffType buffType)
    {
        List buffdatas = new List();
        for (int i = 0; i < buffs.Count; i++)
        {
            if (buffs[i].BuffType == buffType)
                buffdatas.Add(buffs[i]);
        }
        return buffdatas.ToArray();
    }

    /// 
    /// 执行buff
    /// 
    /// 
    public void DoBuff(int buffID)
    {
        BuffManager.Instance.DoBuff(Actor,buffID);
    }

}

[Serializable]
public class BuffData
{
    /// 
    /// 缓存栈
    /// 
    private static Stack poolCache = new Stack();
    /// 
    /// BuffData下一个ID
    /// 
    public static int buffIndex { get; private set; }
    /// 
    /// ID
    /// 
    public int buffDataID;
    /// 
    /// 配置表ID
    /// 
    public int BuffID;
    /// 
    /// buff类型
    /// 
    public BuffType BuffType;
    /// 
    /// 叠加类型
    /// 
    public BuffOverlap BuffOverlap = BuffOverlap.StackedLayer;
    /// 
    /// 执行次数
    /// 
    public BuffCalculateType BuffCalculate = BuffCalculateType.Loop;
    /// 
    /// 关闭类型
    /// 
    public BuffShutDownType BuffShutDownType = BuffShutDownType.All;
    /// 
    /// 最大限制
    /// 
    public int MaxLimit;
    /// 
    /// 当前数据
    /// 
    [SerializeField]
    private int _Limit;
    public int GetLimit { get { return _Limit; } }
    /// 
    /// 执行时间
    /// 
    [SerializeField]
    private float PersistentTime;
    public float GetPersistentTime { get { return PersistentTime; } }
    /// 
    /// 当前时间
    /// 
    [SerializeField]
    private float _CurTime;
    /// 
    /// 事件参数
    /// 
    public object Data;
    /// 
    /// 调用频率
    /// 
    public float CallFrequency { get; set; }
    /// 
    /// 当前频率
    /// 
    private float _curCallFrequency { get; set; }
    /// 
    /// 执行次数
    /// 
    [SerializeField]
    private int index = 0;
    /// 
    /// 根据 CallFrequency 间隔 调用 结束时会调用一次 会传递 Data数据
    /// 
    public Action<object> OnCallBackParam;
    /// 
    ///   /// 
    /// 根据 CallFrequency 间隔 调用 结束时会调用一次 会传递 Data数据 int 次数
    /// 
    /// 
    public Action<object, int> OnCallBackParamIndex;
    /// 
    /// 根据 CallFrequency 间隔 调用 结束时会调用一次
    /// 
    public Action OnCallBack;
    /// 
    /// 根据 CallFrequency 间隔 调用 结束时会调用一次 int 次数
    /// 
    public Action<int> OnCallBackIndex;

    /// 
    /// 当改变时间
    /// 
    public Action OnChagneTime;
    /// 
    /// 当添加层
    /// 
    public Action OnAddLayer;
    /// 
    /// 当删除层
    /// 
    public Action OnSubLayer;
    /// 
    /// 开始调用
    /// 
    public Action OnStart;
    /// 
    /// 结束调用
    /// 
    public Action OnFinsh;
    [SerializeField]
    private bool _isFinsh;

    /// 
    /// 构造方法
    /// 
    private BuffData() {
        buffDataID = buffIndex++;
        CallFrequency = 1;
        PersistentTime = 0;
    }

    private BuffData(float persistentTime,Action onCallBack)
    {

        PersistentTime = persistentTime;
        OnCallBack = onCallBack;
        buffDataID = buffIndex++;
    }

    /// 
    /// 重置时间
    /// 
    public void ResterTime()
    {
        _CurTime = 0;
    }

    /// 
    /// 修改 时间
    /// 
    /// 
    public void ChangePersistentTime(float time)
    {
        PersistentTime = time;
        if (PersistentTime >= MaxLimit)
            PersistentTime = MaxLimit;
        if (OnChagneTime != null)
            OnChagneTime(this);
    }

    /// 
    /// 加一层
    /// 
    public void AddLayer()
    {
        _Limit++;
        _CurTime = 0;
        if (_Limit > MaxLimit)
        {
            _Limit = MaxLimit;
            return;
        }
        if (OnAddLayer != null)
            OnAddLayer(this);
    }

    /// 
    /// 减一层
    /// 
    public void SubLayer()
    {
        _Limit--;
        if (OnSubLayer != null)
            OnSubLayer(this);
    }

    /// 
    /// 开始Buff
    /// 
    public void StartBuff()
    {
        //ChangeLimit(MaxLimit);
        _isFinsh = false;
        if (OnStart != null)
            OnStart();
    }

    /// 
    /// 执行中
    /// 
    /// 
    public void OnTick(float delayTime)
    {
        _CurTime += delayTime;
        //判断时间是否结束
        if (_CurTime >= PersistentTime)
        {
            ///调用事件
            CallBack();
            //判断结束类型 为层方式
            if (BuffShutDownType == BuffShutDownType.Layer)
            {
                SubLayer();
                //判读层数小于1 则结束
                if (_Limit <= 0)
                {
                    _isFinsh = true;
                    return;
                }
                //重置时间
                _curCallFrequency = 0;
                _CurTime = 0;
                return;
            }
            _isFinsh = true;
            return;
        }

        //如果是按频率调用
        if (CallFrequency > 0)
        {
            _curCallFrequency += delayTime;
            if (_curCallFrequency >= CallFrequency)
            {
                _curCallFrequency = 0;
                CallBack();
            }
            return;
        }
        ///调用回调
        CallBack();
    }

    /// 
    /// 获取当前执行时间
    /// 
    public float GetCurTime
    {
        get { return _CurTime; }
    }
    /// 
    /// 是否结束
    /// 
    public bool IsFinsh
    {
        get { return _isFinsh; }
    }

    /// 
    /// 调用回调
    /// 
    private void CallBack()
    {
        //次数增加
        index++;
        //判断buff执行次数 
        if (BuffCalculate == BuffCalculateType.Once)
        {
            if (index > 1) { index = 2; return; }
        }

        if (OnCallBack != null)
            OnCallBack();
        if (OnCallBackIndex != null)
            OnCallBackIndex(index);
        if (OnCallBackParam != null)
            OnCallBackParam(Data);
        if (OnCallBackParamIndex != null)
            OnCallBackParamIndex(Data, index);
    }

    /// 
    /// 关闭buff
    /// 
    public void CloseBuff()
    {
        if (OnFinsh != null)
            OnFinsh();
        Clear();
    }

    public void Clear()
    {
        _Limit = 0;
        BuffID = -1;
        index = 0;
        PersistentTime = 0;
        _CurTime = 0;
        Data = null;
        CallFrequency = 0;
        _curCallFrequency = 0;
        OnCallBackParam = null;
        OnCallBack = null;
        OnStart = null;
        OnFinsh = null;
        _isFinsh = false;
        Push(this);
    }

    /// 
    /// 创建BuffData
    /// 
    /// 
    public static BuffData Create()
    {
        if (poolCache.Count < 1)
            return new BuffData();
        BuffData buffData = poolCache.Pop();
        return buffData;
    }

    public static BuffData Create(BuffBase buffBase,Action onCallBack)
    {
       return Create(buffBase,onCallBack,null,null);
    }

    public static BuffData Create(BuffBase buffBase, Action onCallBack,Action addLayerAcion,Action subLayerAction)
    {
        BuffData db = Pop();
        db.BuffCalculate = buffBase.BuffCalculate;
        db.BuffID = buffBase.BuffID;
        db.CallFrequency = buffBase.CallFrequency;
        db.PersistentTime = buffBase.Time;
        db.BuffOverlap = buffBase.BuffOverlap;
        db.BuffShutDownType = buffBase.BuffShutDownType;
        db.BuffType = buffBase.BuffType;
        db.MaxLimit = buffBase.MaxLimit;
        db.OnCallBack = onCallBack;
        db.OnAddLayer = addLayerAcion;
        db.OnSubLayer = subLayerAction;
        db._Limit = 1;
        return db;
    }

    /// 
    /// 弹出
    /// 
    /// 
    private static BuffData Pop()
    {
        if (poolCache.Count < 1)
        {
            BuffData bd = new BuffData();
            return bd;
        }
        BuffData buffData = poolCache.Pop();
        return buffData;
    }

    /// 
    /// 压入
    /// 
    /// 
    private static void Push(BuffData buffData)
    {
        poolCache.Push(buffData);
    }

}


OK,到此,大部分功能都做完了,当然还有很多需要完善的部分,不过这毕竟是个人作品,没动力啊。buff系统大概流程就是如此了,在上述提供的列子中还有浮空的Buff等等,大家就需要凭才华进行制作了。

这样我们已经完成,UI系统,关卡系统,Buff系统,剧情系统,寻路系统,还有配置表。等明年回来我在完善一下任务系统,指引系统,技能系统,Ai系统和状态机,这样一个游戏大部分功能就都已经实现了。

预热技能系统:
Unity3D 实现简单的Buff系统_第10张图片

你可能感兴趣的:(Unity3D技术专区,Buff,Unity3d)