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

Basic Field Description
The following are the basic field descriptions for Buff configuration:
Field | Description | Example |
---|---|---|
Id | Unique identifier for the Buff | 1 |
Name | Name of the Buff | Life Drain |
Desc | Detailed description of the Buff | Reduces maximum health by 3% per second, lasts for 3 seconds, stacks up to 1 layer |
BuffInfoConfig | Detailed configuration items | See detailed explanation below |
Icon | Buff icon resource name | UI_Skill_Icon_9 |
BuffInfoConfig Detailed Configuration
BuffInfoConfig contains the core configuration parameters for the Buff:
Configuration Item | Description | Possible Values |
---|---|---|
Numeric ID | Affected attribute ID | 1001=Attack Power 1002=Maximum Health 1003=Defense |
Buff Addition Type | How to handle when the buff is added repeatedly | 1=Reset Time 2=Stack Layers 3=Stack Layers and Reset Time |
Buff Value | Effect value (actual value obtained after dividing by 10000) | 30000=3% 50000=5% |
Duration | Buff duration (seconds) | 3=3 seconds 5=5 seconds |
Buff Execution Type | How the buff is executed | |
Cycle End Execution Type | How to handle when the buff ends | |
Interval | Time interval for periodic execution (seconds) | 1=execute once per second |
Maximum Layers | Maximum 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:
- This Buff reduces the target's maximum health by 3% every second (interval=1)
- The effect lasts for 3 seconds
- Cannot stack (maximum layers=1)
- The effect is directly removed when time expires (cycle end execution type=1)
- 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