1. Dots资源包导入
Window--->Package Manager,导入Jobs,Entities,Hybrid Renderer,Burst,Entities Graphics




2.Luban导入
按照Luban官方文档导入即可:https://www.datable.cn/docs/guide/install
3. 方案与思路
配置表上以Buff表和BuffEffect表为核心
Buff表字段,配置Buff的时间,单次触发时间,是否叠加层数等

BuffEffect表字段,配置Buff的具体效果

枚举值需要用到Luban的__enums__表

Dots方面设计思路
Component有AddBuffComponent,BuffComponent,BuffTriggerEventComponent
System有AddBuffSystem,BuffExpireSystem,BuffTickSystem,BuffEffectSystem
执行顺序为:
给某个实体添加AddBuffComponent ------> AddBuffSystem添加具体Buff ------> BuffExpireSystem检查Buff是否过期 ------> BuffTickSystem检查OnTick触发条件的Buff ------> BuffEffectSystem具体实现Buff效果
AddBuffComponent
csharp
public struct AddBuffComponent : IBufferElementData
{
public Entity Target;
public Entity Caster;
public int BuffId;
}
BuffTriggerEventComponent
csharp
public struct BuffTriggerEventComponent : IBufferElementData
{
public Entity Caster;
public Entity Target;
public int BuffId;
public uint BuffInstanceId;
// 触发瞬间的快照;即使 Buff 已移除,OnExpire 仍能正确结算。
public int Stack;
public BuffTriggerType TriggerType;
}
public enum BuffTriggerType : byte
{
OnApply = 1,
OnTick = 2,
OnExpire = 8,
OnRemove = 3,
OnHit = 5,
OnDamaged = 6,
OnKill = 7,
OnStackChanged = 9
}
BuffComponent
csharp
public struct BuffComponent : IBufferElementData
{
public int BuffID;
public Entity Caster;
public uint InstanceId;
public int Stack;
public float StartTime;
public float EndTime;
public float NextTickTime;
}
AddBuffSystem
csharp
[UpdateInGroup(typeof(BuffSystemGroup))]
[UpdateBefore(typeof(BuffExpireSystem))]
public partial struct AddBuffSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
state.RequireForUpdate<AddBuffComponent>();
}
public void OnUpdate(ref SystemState state)
{
foreach (var (addBuffs, entity) in SystemAPI.Query<DynamicBuffer<AddBuffComponent>>().WithEntityAccess())
{
var time = (float)SystemAPI.Time.ElapsedTime;
for (int i = 0; i < addBuffs.Length; i++)
{
var addBuff = addBuffs[i];
if (!BuffUtility.TryGetBuffConfig(addBuff.BuffId, out var buffConfig))
{
DotsLogger.Info($"AddBuffSystem Buff config `{addBuff.BuffId}` doesn't exist");
continue;
}
if (!SystemAPI.HasBuffer<BuffComponent>(addBuff.Target))
{
DotsLogger.Info($"AddBuffSystem Buff `{addBuff.Target}` doesn't exist");
continue;
}
var buffs = SystemAPI.GetBuffer<BuffComponent>(addBuff.Target);
var newBuff = BuffUtility.AddBuff(buffs, addBuff.BuffId, addBuff.Caster, time, buffConfig);
BuffUtility.TryGetBuffEffectConfig(buffConfig.BuffEffectID, out var buffEffectConfig);
if (buffEffectConfig.TriggerType == (int)BuffTriggerType.OnApply)
{
var eventBuffer = SystemAPI.GetSingletonBuffer<BuffTriggerEventComponent>();
eventBuffer.Add(new BuffTriggerEventComponent
{
Target = addBuff.Target,
Caster = addBuff.Target,
BuffId = addBuff.BuffId,
BuffInstanceId = newBuff.InstanceId,
Stack = newBuff.Stack,
TriggerType = BuffTriggerType.OnApply
});
}
}
addBuffs.Clear();
}
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
}
BuffExpireSystem
csharp
[UpdateInGroup(typeof(BuffSystemGroup))]
[UpdateAfter(typeof(AddBuffSystem))]
public partial struct BuffExpireSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
}
public void OnUpdate(ref SystemState state)
{
var now = (float)SystemAPI.Time.ElapsedTime;
var triggerEvents =
SystemAPI.GetSingletonBuffer<BuffTriggerEventComponent>();
foreach (var (buffs, target) in SystemAPI.Query<DynamicBuffer<BuffComponent>>().WithEntityAccess())
{
// 倒序移除,避免索引错位。
for (var i = buffs.Length - 1; i >= 0; i--)
{
var buff = buffs[i];
if (!BuffUtility.IsExpired(buff, now))
{
continue;
}
// 必须在移除前记录快照。
triggerEvents.Add(new BuffTriggerEventComponent
{
Target = target,
Caster = buff.Caster,
BuffId = buff.BuffID,
BuffInstanceId = buff.InstanceId,
Stack = buff.Stack,
TriggerType = BuffTriggerType.OnExpire
});
var modifiers = SystemAPI.GetBuffer<AttributeModifier>(target);
for (var modifierIndex = modifiers.Length - 1; modifierIndex >= 0; modifierIndex--)
{
if (modifiers[modifierIndex].SourceId == buff.InstanceId)
{
modifiers.RemoveAt(modifierIndex);
}
}
buffs.RemoveAt(i);
}
}
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
}
BuffTickSystem
csharp
[UpdateInGroup(typeof(BuffSystemGroup))]
[UpdateAfter(typeof(BuffExpireSystem))]
[UpdateBefore(typeof(BuffEffectSystem))]
public partial struct BuffTickSystem : ISystem
{
private const int MaxTicksPerFrame = 3;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
}
public void OnUpdate(ref SystemState state)
{
var now = (float)SystemAPI.Time.ElapsedTime;
var eventBuffer = SystemAPI.GetSingletonBuffer<BuffTriggerEventComponent>();
foreach (var (buffs, target) in SystemAPI.Query<DynamicBuffer<BuffComponent>>().WithEntityAccess())
{
var buffBuffer = buffs;
var time = (float)SystemAPI.Time.ElapsedTime;
for (int i = buffs.Length - 1; i >= 0 ; i--)
{
var buff = buffs[i];
if (!BuffUtility.TryGetBuffConfig(buff.BuffID, out var config))
{
continue;
}
if (config.TickInterval <= 0f || now < buff.NextTickTime)
{
continue;
}
BuffUtility.TryGetBuffEffectConfig(config.BuffEffectID, out var effectConfig);
if (effectConfig.TriggerType != (int)BuffTriggerType.OnTick)
{
continue;
}
var tickCount = 0;
while (now >= buff.NextTickTime &&
tickCount < MaxTicksPerFrame)
{
eventBuffer.Add(new BuffTriggerEventComponent
{
Target = target,
Caster = buff.Caster,
BuffId = buff.BuffID,
BuffInstanceId = buff.InstanceId,
Stack = buff.Stack,
TriggerType = BuffTriggerType.OnTick
});
// 从旧 Tick 时间推进,避免掉帧导致 Tick 时间漂移。
buff.NextTickTime += config.TickInterval;
tickCount++;
}
buffBuffer[i] = buff;
}
}
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
}
BuffEffectSystem
csharp
[UpdateInGroup(typeof(BuffSystemGroup))]
[UpdateAfter(typeof(BuffTickSystem))]
public partial struct BuffEffectSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
}
public void OnUpdate(ref SystemState state)
{
var triggers = SystemAPI.GetSingletonBuffer<BuffTriggerEventComponent>();
for (int i = 0; i < triggers.Length; i++)
{
var trigger = triggers[i];
bool hasConfig = BuffUtility.TryGetBuffConfig(trigger.BuffId, out var buffConfig);
if (!hasConfig)
{
continue;
}
bool hasEffectConfig = BuffUtility.TryGetBuffEffectConfig(buffConfig.BuffEffectID, out var buffEffectConfig);
if (!hasEffectConfig || buffEffectConfig.TriggerType != (int)trigger.TriggerType)
{
continue;
}
DotsLogger.Info($"Buff Effect Config: {buffEffectConfig.BuffEffectID}, effectType : {buffEffectConfig.EffectType}");
switch ((BuffEffectConfigEffectType)buffEffectConfig.EffectType)
{
case BuffEffectConfigEffectType.Damage:
AddDamage(ref state, trigger.Target, buffEffectConfig.Value);
break;
case BuffEffectConfigEffectType.Health:
AddHealth(ref state, trigger.Target, buffEffectConfig.Value);
break;
case BuffEffectConfigEffectType.AttributeModifier:
var modifiers = SystemAPI.GetBuffer<AttributeModifier>(trigger.Target);
modifiers.Add(new AttributeModifier
{
AttributeType = (AttributeType)buffEffectConfig.AttributeType,
Operation = (AttributeModifierOperation)buffEffectConfig.ValueMode,
Value = buffEffectConfig.Value,
SourceId = trigger.BuffInstanceId,
SourceEntity = trigger.Caster
});
break;
}
}
triggers.Clear();
}
[BurstCompile]
public void OnDestroy(ref SystemState state)
{
}
private void AddDamage(ref SystemState state, Entity entity, float damage)
{
ChangeHealth(ref state, entity, damage);
}
private void AddHealth(ref SystemState state, Entity entity, float heal)
{
ChangeHealth(ref state, entity, heal);
}
private void ChangeHealth(ref SystemState state, Entity entity, float value)
{
if (!state.EntityManager.HasComponent<VitalComponent>(entity))
{
return;
}
var vital = state.EntityManager.GetComponentData<VitalComponent>(entity);
var maxHealth = state.EntityManager.HasComponent<AttributeComponent>(entity)
? state.EntityManager.GetComponentData<AttributeComponent>(entity).MaxHealth
: float.MaxValue;
vital.Health = math.clamp(vital.Health + value, 0f, maxHealth);
state.EntityManager.SetComponentData(entity, vital);
}
}
由于Luban导出的模板表数据不能在Dots层直接使用,因此做了一层转换
csharp
public static class BuffUtility
{
private static BuffConfigData _buffConfig;
private static bool _isCreateConfig;
public static void InitBuffConfig()
{
if (!_isCreateConfig)
{
var allTemp = TemplateManager.Instance.Tables.TbBuff.DataList;
NativeHashMap<int, BuffRunTimeConfig> buffConfig = new NativeHashMap<int, BuffRunTimeConfig>(allTemp.Count, Allocator.Persistent);
for (int i = 0; i < allTemp.Count; i++)
{
var runTimeConfig = new BuffRunTimeConfig();
runTimeConfig.BuffId = allTemp[i].Id;
runTimeConfig.Duration = allTemp[i].Duration;
runTimeConfig.TickInterval = allTemp[i].TickInterval;
runTimeConfig.MaxStack = allTemp[i].MaxStack;
runTimeConfig.StackType = allTemp[i].StackType;
runTimeConfig.BuffEffectID = allTemp[i].Effect;
buffConfig.Add(allTemp[i].Id, runTimeConfig);
}
var buffEffectTempList = TemplateManager.Instance.Tables.TbBuffEffect.DataList;
NativeHashMap<int, BuffEffectConfig> buffEffectConfigList = new NativeHashMap<int, BuffEffectConfig>(buffEffectTempList.Count, Allocator.Persistent);
for (int i = 0; i < buffEffectTempList.Count; i++)
{
var buffEffectConfig = new BuffEffectConfig();
buffEffectConfig.BuffEffectID = buffEffectTempList[i].Id;
buffEffectConfig.TriggerType = (int)buffEffectTempList[i].Trigger;
buffEffectConfig.TargetSelecor = (int)buffEffectTempList[i].TargetSelecor;
buffEffectConfig.EffectType = (int)buffEffectTempList[i].EffectType;
buffEffectConfig.ValueMode = (int)buffEffectTempList[i].ValueMode;
buffEffectConfig.AttributeType = (int)buffEffectTempList[i].AttributeType;
buffEffectConfig.Value = buffEffectTempList[i].Value;
buffEffectConfigList.Add(buffEffectTempList[i].Id, buffEffectConfig);
}
_buffConfig = new BuffConfigData
{
Config = buffConfig, EffectConfig = buffEffectConfigList
};
_isCreateConfig = true;
}
}
public static bool TryGetBuffConfig(
int buffId,
out BuffRunTimeConfig config)
{
if (!_isCreateConfig)
{
InitBuffConfig();
}
return _buffConfig.Config.TryGetValue(buffId, out config);
}
public static bool TryGetBuffEffectConfig(int effectID, out BuffEffectConfig config)
{
if (!_isCreateConfig)
{
InitBuffConfig();
}
return _buffConfig.EffectConfig.TryGetValue(effectID, out config);
}
}