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.