Skip to content

Buff System ​

Introduction ​

The Buff system provides a simple status management functionality for handling basic status effects on characters. For example:

  • Beneficial effects: Attack power increase, defense increase, etc.
  • Debuffs: Poison, slow, etc.

The system supports:

  • Basic duration management
  • Simple buff addition and removal

Note

If it doesn't meet your requirements, you'll need to modify it yourself

Core Code ​

Entity Definitions ​

BuffInfo.cs
csharp
using System.Collections.Generic;
using cfg;
using Newtonsoft.Json;
namespace MH
{
    public class BuffInfo : Entity, IAwake, IAwake<int, Unit, Unit>, IDestroy
    {
        /// <summary>
        /// Timer ID
        /// </summary>
        public long TimerId;
        /// <summary>
        /// Creation time
        /// </summary>
        public long CreateTime;
        /// <summary>
        /// End time
        /// </summary>
        public long EndTime;
        /// <summary>
        /// Config ID
        /// </summary>
        public int ConfigId;
        /// <summary>
        /// Layer count
        /// </summary>
        public int Layer;
        /// <summary>
        /// Caster
        /// </summary>
        public Unit Caster;
        /// <summary>
        /// Target
        /// </summary>
        public Unit Target;
        /// <summary>
        /// Configuration
        /// </summary>
        [JsonIgnore]
        public BuffConfig Config => ConfigsSingleton.Instance.Tables.TbBuffConfig.Get(ConfigId);
    }
    /// <summary>
    /// Buff addition type
    /// </summary>
    public enum BuffAddType
    {
        ResetTime = 1,                     //Reset buff time
        MultipleLayer,                 //Increase buff layers
        MultipleLayerAndResetTime,     //Increase buff layers and reset buff time
    }
    /// <summary>
    /// Buff action type
    /// </summary>
    public enum BuffActionType
    {
        Add = 1,
        Sub,
        Override,
    }
    /// <summary>
    /// Cycle end type
    /// </summary>
    public enum CycleEndType
    {
        Sub = 1,
        Clear,
    }
}
BuffInfosComponent.cs
csharp
using System.Collections.Generic;
namespace MH
{
    public class BuffInfosComponent: Entity, IAwake, IDestroy
    {
        /// <summary>
        /// Buff list
        /// </summary>
        public List<BuffInfo> BuffInfos = new List<BuffInfo>();
    }
}

System Definitions ​

BuffInfoSystem.cs
csharp
using System.Collections.Generic;
using UnityEngine;

namespace MH
{
    [Invoke(TimerInvokeType.BuffActionIntervalTimerInvoke)]
    public class Buff_TimerAction : ATimer<BuffInfo>
    {
        protected override void Run(BuffInfo self)
        {
            self.TryStartCountDown();
        }
    }
    [EntitySystem]
    public class BuffInfoAwakeSystem: AwakeSystem<BuffInfo, int, Unit, Unit>
    {
        protected override void Awake(BuffInfo self, int configId, Unit caster, Unit target)
        {
            self.ConfigId = configId;
            self.CreateTime = TimeInfo.Instance.ClientNow();
            self.EndTime = self.Config.BuffInfoConfig.BuffInfoTimer * 1000 + self.CreateTime;
            self.Caster = caster;
            self.Target = target;
            self.Layer = 1;
            BuffActionDispatcher.Instance.OnStart(self);
            self.TryStartCountDown();
        }
    }
    
    [EntitySystem]
    public class BuffInfoDestroySystem: DestroySystem<BuffInfo>
    {
        protected override void Destroy(BuffInfo self)
        {
            BuffActionDispatcher.Instance.OnEnd(self);
            self.EndTime = 0;
            self.ConfigId = 0;
            self.Target = null;
            self.Caster = null;
            self.Layer = 0;
            self.Root.GetComponent<TimerComponent>().Remove(ref self.TimerId);
        }
    }

    public static class BuffInfoSystem
    {
        /// <summary>
        /// Repeatedly add buff
        /// </summary>
        /// <param name="self"></param>
        public static void RepeatAddBuffInfo(this BuffInfo self)
        {
            int oldLayer = self.Layer;
            switch (self.Config.BuffInfoConfig.BuffAddType)
            {
                case (int)BuffAddType.ResetTime:
                    self.EndTime = self.Config.BuffInfoConfig.BuffInfoTimer * 1000 + TimeInfo.Instance.ClientNow();
                    break;
                case (int)BuffAddType.MultipleLayer:
                    if (self.Layer < self.Config.BuffInfoConfig.MaxLayer)
                        self.Layer++;
                    break;
                case (int)BuffAddType.MultipleLayerAndResetTime:
                    if (self.Layer < self.Config.BuffInfoConfig.MaxLayer)
                        self.Layer++;
                    self.EndTime = self.Config.BuffInfoConfig.BuffInfoTimer * 1000 + TimeInfo.Instance.ClientNow();
                    break;
            }
            //If layer count changes, call BuffActionDispatcher's OnLevelChange method
            if (oldLayer != self.Layer)
                BuffActionDispatcher.Instance.OnLevelChange(self, oldLayer, self.Layer);
        }
        /// <summary>
        /// Try to start the countdown
        /// </summary>
        /// <param name="self"></param>
        public static void TryStartCountDown(this BuffInfo self)
        {
            if (!self.TryCompleted())
            {
                var timerComponent = self.Root.GetComponent<TimerComponent>();
                self.TimerId = timerComponent.NewOnceTimer(TimeInfo.Instance.ClientNow() + (int)(self.Config.BuffInfoConfig.Interval * 1000f),
                        TimerInvokeType.BuffActionIntervalTimerInvoke, self);
                BuffActionDispatcher.Instance.Run(self);
                return;
            }
            //If buff is completed, remove it
            BuffFactory.RemoveBuff(self);
        }
        /// <summary>
        /// Try to complete buff
        /// </summary>
        /// <param name="self"></param>
        /// <returns></returns>
        public static bool TryCompleted(this BuffInfo self)
        {
            int oldLayer = self.Layer;
            if (self.EndTime > TimeInfo.Instance.ClientNow())
                return false;
            //Still has layers
            if (self.Config.BuffInfoConfig.CycleEndType == (int)CycleEndType.Clear)
                self.Layer = 0;
            else if (self.Config.BuffInfoConfig.CycleEndType == (int)CycleEndType.Sub)
            {
                self.Layer -= 1;
                self.EndTime = self.Config.BuffInfoConfig.BuffInfoTimer * 1000 + TimeInfo.Instance.ClientNow();
            }
            //Destroy
            if (self.Layer > 0)
                return false;
            //If layer count changes, call BuffActionDispatcher's OnLevelChange method
            if (oldLayer != self.Layer)
                BuffActionDispatcher.Instance.OnLevelChange(self, oldLayer, self.Layer);
            return true;
        }
    }
}
BuffInfosComponentSystem.cs
csharp
namespace MH
{
    public static class BuffInfosComponentSystem
    {
        /// <summary>
        /// Add or update buff
        /// </summary>
        /// <param name="self"></param>
        /// <param name="buff">buff</param>
        public static void AddOrUpdate(this BuffInfosComponent self, BuffInfo buff)
        {
            BuffInfo old = self.GetByConfigId(buff.ConfigId);
            if (old != null)
            {
                old.RepeatAddBuffInfo();
                buff?.Dispose();
                return;
            }
            self.BuffInfos.Add(buff);
        }
        /// <summary>
        /// Get buff by ID
        /// </summary>
        /// <param name="self"></param>
        /// <param name="buffInfoId">buffId</param>
        /// <returns></returns>
        public static BuffInfo GetById(this BuffInfosComponent self, long buffInfoId)
        {
            foreach (BuffInfo buffInfo in self.BuffInfos)
            {
                if (buffInfo.Id == buffInfoId)
                    return buffInfo;
            }
            return null;
        }
        /// <summary>
        /// Get buff by config ID
        /// </summary>
        /// <param name="self"></param>
        /// <param name="configId">Config table Id</param>
        /// <returns></returns>
        public static BuffInfo GetByConfigId(this BuffInfosComponent self, int configId)
        {
            foreach (BuffInfo buffInfo in self.BuffInfos)
            {
                if (buffInfo.ConfigId == configId)
                    return buffInfo;
            }
            return null;
        }
        /// <summary>
        /// Remove buff
        /// </summary>
        /// <param name="self"></param>
        /// <param name="buffInfoId">buffId</param>
        public static void Remove(this BuffInfosComponent self, long buffInfoId)
        {
            BuffInfo buffInfo = self.GetById(buffInfoId);
            if (buffInfo != null)
            {
                self.BuffInfos.Remove(buffInfo);
                buffInfo?.Dispose();
            }
        }
    }
}
BuffFactory.cs
csharp
namespace MH
{
    public static class BuffFactory
    {
        /// <summary>
        /// Create buff
        /// </summary>
        /// <param name="caster"></param>
        /// <param name="target"></param>
        /// <param name="configId"></param>
        public static void CreateBuff(Unit caster, Unit target, int configId)
        {
            var buffInfosComponent = target.GetComponent<BuffInfosComponent>();
            var info = buffInfosComponent.AddChild<BuffInfo, int, Unit, Unit>(configId, caster, target);
            buffInfosComponent.AddOrUpdate(info);
        }

        /// <summary>
        /// Remove buff
        /// </summary>
        /// <param name="buffInfo"></param>
        public static void RemoveBuff(BuffInfo buffInfo)
        {
            buffInfo.GetParent<BuffInfosComponent>().Remove(buffInfo.Id);
        }
        /// <summary>
        /// Buff layer change
        /// </summary>
        /// <param name="buffInfo"></param>
        /// <param name="oldLayer"></param>
        /// <param name="newLayer"></param>
        public static void OnBuffLayerValueChange(BuffInfo buffInfo, int oldLayer, int newLayer)
        {
            var attributeModifierComponent = buffInfo.Target.GetComponent<AttributeModifierComponent>();
            if (attributeModifierComponent == null)
                return;
            if (newLayer > oldLayer)
            {
                for (int i = 0; i < buffInfo.Config.BuffInfoConfig.NumericTypes.Count; i++)
                {
                    var key = buffInfo.Config.BuffInfoConfig.NumericTypes[i];
                    var value = buffInfo.Config.BuffInfoConfig.NumericValues[i];
                    attributeModifierComponent.AddModifier(key, buffInfo.Id, value, ModifierType.Add);
                }
            }
            else
            {
                for (int i = 0; i < buffInfo.Config.BuffInfoConfig.NumericTypes.Count; i++)
                {
                    var key = buffInfo.Config.BuffInfoConfig.NumericTypes[i];
                    var value = buffInfo.Config.BuffInfoConfig.NumericValues[i];
                    attributeModifierComponent.AddModifier(key, buffInfo.Id, -value, ModifierType.Add);
                }
            }
        }
        /// <summary>
        /// Buff start
        /// </summary>
        /// <param name="buffInfo"></param>
        /// <param name="modifierType"></param>
        public static void OnBuffStart(BuffInfo buffInfo, ModifierType modifierType)
        {
            var attributeModifierComponent = buffInfo.Target.GetComponent<AttributeModifierComponent>();
            if (attributeModifierComponent == null)
                return;
            for (int i = 0; i < buffInfo.Config.BuffInfoConfig.NumericTypes.Count; i++)
            {
                var key = buffInfo.Config.BuffInfoConfig.NumericTypes[i];
                var value = buffInfo.Config.BuffInfoConfig.NumericValues[i];
                attributeModifierComponent.AddModifier(key, buffInfo.Id, value, modifierType);
            }
        }
    }
}

Buff Lifecycle Event Dispatcher Component ​

BuffActionDispatcher.cs

csharp
using System;
using System.Collections.Generic;

namespace MH
{
    public class BuffActionDispatcher : LogicSingleton<BuffActionDispatcher>, ISingletonAwake
    {
        private Dictionary<BuffInfoType, IBuffAction> buffActions = new Dictionary<BuffInfoType, IBuffAction>();

        public void Awake()
        {
            HashSet<Type> types = CodeTypes.Instance.GetTypes(typeof(BuffActionAttribute));
            foreach (Type type in types)
            {
                object[] attrs = type.GetCustomAttributes(typeof(BuffActionAttribute), false);
                foreach (object attr in attrs)
                {
                    BuffActionAttribute buffActionAttribute = (BuffActionAttribute)attr;
                    IBuffAction obj = (IBuffAction)Activator.CreateInstance(type);
                    if (this.buffActions.ContainsKey(buffActionAttribute.buffInfoType))
                        throw new Exception($"BuffActionDispatcher already contains a BuffAction with BuffInfoType {buffActionAttribute.buffInfoType}");
                    this.buffActions[buffActionAttribute.buffInfoType] = obj;
                }
            }
        }
        public void Run(BuffInfo buffInfo)
        {
            if (this.buffActions.TryGetValue((BuffInfoType)buffInfo.Config.Id, out IBuffAction buffAction))
            {
                buffAction.Run(buffInfo);
            }
        }
        public void OnStart(BuffInfo buffInfo)
        {
            if (this.buffActions.TryGetValue((BuffInfoType)buffInfo.Config.Id, out IBuffAction buffAction))
            {
                buffAction.OnStart(buffInfo);
            }
        }
        public void OnEnd(BuffInfo buffInfo)
        {
            if (this.buffActions.TryGetValue((BuffInfoType)buffInfo.Config.Id, out IBuffAction buffAction))
            {
                buffAction.OnEnd(buffInfo);
            }
        }
        public void OnLevelChange(BuffInfo buffInfo, int oldLayer, int newLayer)
        {
            if (this.buffActions.TryGetValue((BuffInfoType)buffInfo.Config.Id, out IBuffAction buffAction))
            {
                buffAction.OnLevelChange(buffInfo, oldLayer, newLayer);
            }
        }
    }
}

This component collects buff handler classes with the BuffAction tag during Awake and caches them for easy access

Configuration Description ​

Buff Configuration Overview ​

Buff Configuration Information

Basic Field Description ​

The following are the basic field descriptions for Buff configuration:

FieldDescriptionExample
IdUnique identifier for the Buff1
NameName of the BuffLife Drain
DescDetailed description of the BuffReduces maximum health by 3% per second, lasts for 3 seconds, stacks up to 1 layer
BuffInfoConfigDetailed configuration itemsSee detailed explanation below
IconBuff icon resource nameUI_Skill_Icon_9

BuffInfoConfig Detailed Configuration ​

BuffInfoConfig contains the core configuration parameters for the Buff:

Configuration ItemDescriptionPossible Values
Numeric IDAffected attribute ID1001=Attack Power
1002=Maximum Health
1003=Defense
Buff Addition TypeHow to handle when the buff is added repeatedly1=Reset Time
2=Stack Layers
3=Stack Layers and Reset Time
Buff ValueEffect value (actual value obtained after dividing by 10000)30000=3%
50000=5%
DurationBuff duration (seconds)3=3 seconds
5=5 seconds
Buff Execution TypeHow the buff is executed
Cycle End Execution TypeHow to handle when the buff ends
IntervalTime interval for periodic execution (seconds)1=execute once per second
Maximum LayersMaximum number of stacking layers for the buff

Any value changes should be done in conjunction with the numeric component

Configuration Example ​

Id: 1
Name: Life Drain
Desc: Reduces maximum health by 3% per second, lasts for 3 seconds, stacks up to 1 layer
BuffInfoConfig: {
    Numeric ID: 1002,        // Affects health
    Buff Addition Type: 1,   // Reset time when added repeatedly
    Buff Value: 30000,       // 3%
    Duration: 3,             // 3 seconds
    Buff Execution Type: 3,  // Periodic execution
    Cycle End Execution Type: 1, // Remove directly when time expires
    Interval: 1,             // Execute every second
    Maximum Layers: 1        // Cannot stack
}
Icon: UI_Skill_Icon_9

Effect description:

  1. This Buff reduces the target's maximum health by 3% every second (interval=1)
  2. The effect lasts for 3 seconds
  3. Cannot stack (maximum layers=1)
  4. The effect is directly removed when time expires (cycle end execution type=1)
  5. If added repeatedly, the duration is refreshed (buff addition type=1)

Usage Example ​

1. Specific Buff Handler Implementation ​

csharp
namespace MH
{
    /// <summary>
    /// 每秒降低3%的最大生命值,持续3秒,最高叠加1层
    /// </summary>
    [BuffAction(BuffInfoType.SlowHealth3)]
    public class BuffActionDispatcher_SlowHealth3 : IBuffAction
    {
        public void OnEnd(BuffInfo buffInfo)
        {
            buffInfo.Target.GetComponent<AttributeModifierComponent>()?.ClearSourceModifiers(buffInfo.Id);
        }

        public void OnLevelChange(BuffInfo buffInfo, int oldLayer, int newLayer)
        {
        }

        public void OnStart(BuffInfo buffInfo)
        {
        }

        public void Run(BuffInfo buffInfo)
        {
            var numericComponent = buffInfo.Target.GetComponent<NumericComponent>();
            //首先获取目标的数值组件拿到最大生命值
            var maxHp = numericComponent[NumericType.MaxHp];
            var value = buffInfo.Config.BuffInfoConfig.NumericValues[0];
            //这里再处理一下扣血就可以了
            EventSystem.Instance.PublishAsync(buffInfo.Root, new AttackUnitStartSpecifyDamage()
            {
                TargetUnit = buffInfo.Target,
                Damage = (long)(maxHp * value / 1000000f),
            }).Coroutine();
        }
    }
}
csharp
namespace MH
{
    [Event(SceneType.Main)]
    public class AttackUnitStartSpecifyDamage_EventView : AEvent<Scene, AttackUnitStartSpecifyDamage>
    {
        protected override async ETTask Run(Scene scene, AttackUnitStartSpecifyDamage args)
        {
            if (!args.TargetUnit.IsAlive())
                return;
            long hp = args.TargetUnit.GetComponent<NumericComponent>().GetAsInt(NumericType.Hp);
            hp -= args.Damage;
            if (hp <= 0)
            {
                args.TargetUnit.GetComponent<NumericComponent>()[NumericType.Hp] = 0;
                args.TargetUnit.SetAlive(false);
                return;
            }
            args.TargetUnit.GetComponent<NumericComponent>().Set(NumericType.Hp, hp);
            await ETTask.CompletedTask;
        }
    }
}

Technical Support ​

Get Help

  • 💬 Join QQ group for discussion (ET Framework Group) : 474643097
  • ⭐ Follow the project on GitHub for the latest updates

Released under the MIT License.