Unity里基于Luban的buff系统

基于 Luban 的 Buff 系统使用与扩展指南

本文档详细说明了项目中 Buff 系统的实现原理、使用方法、配置方式以及如何扩展新的 Buff 逻辑。

1. 系统概述

该 Buff 系统是一个数据驱动 的、多态化的运行时效果系统。

  • 配置层:使用 Luban 生成配置 (BuffInfo),支持嵌套 JSON 字符串来定义具体的业务逻辑。
  • 逻辑层:通过 BuffHandler 管理生命周期,BaseBuffModule 定义具体行为。
  • 多态性:Buff 的具体行为(如"护盾"、"造成伤害")通过 JSON 中的 $type 字段动态映射到对应的 C# 类。

2. 核心类说明

类名 路径 说明
BuffHandler HotUpdate/Buff/BuffHandler.cs 挂载在角色/物体上的 MonoBehaviour。管理 Buff 列表,处理 Update (Tick) 和生命周期(添加、移除、叠加)。
BuffInfo HotUpdate/Buff/BuffDesign.cs 运行时 Buff 数据对象。包含配置数据 (BuffData) 以及运行时的计时器、层数、施法者引用等。
BaseBuffModule HotUpdate/Buff/BaseBuffModule.cs 所有具体 Buff 逻辑的基类。定义了 Apply 方法。
BuffConverter HotUpdate/Buff/BuffConverter.cs 负责将配置中的 JSON 字符串解析为具体的 BaseBuffModule 实例。

3. 实现原理

3.1 生命周期回调

Buff 系统定义了多个触发时机(回调点),在配置表中通过 JSON 字符串配置:

  • OnCreate: Buff 被添加或层数叠加时触发。
  • OnTick: 根据 tickTime 间隔周期性触发。
  • OnRemove: Buff 持续时间结束或被手动移除时触发。
  • OnHit: (攻击者视角) 造成攻击时触发。
  • OnBeHurt: (受击者视角) 受到伤害时触发。
  • OnKill: (攻击者视角) 击杀目标时触发。
  • OnBeKill: (受击者视角) 被击杀时触发。

3.2 运行流程

  1. 获取配置:从 GameTables.Instance.Tables.TbBuffInfo 获取原始 BuffInfo。
  2. 实例化模块:调用 BuffConverter.ConvertToBaseBuffModule,解析配置中的 JSON 字符串(如 OnCreate 字段),反序列化为对应的 BaseBuffModule 子类(如 Shield)。
  3. 添加 Buff:调用 buffHandler.AddBuff(buffInfo)。
  4. 触发逻辑:BuffHandler 在特定时机调用对应 Module 的 Apply(buffInfo) 方法执行逻辑。

4. 如何使用 (代码调用)

在代码中给角色添加 Buff 的标准范例:

cs 复制代码
// 1. 获取 BuffHandler 组件

var buffHandler = targetGameObject.

GetComponent<BuffHandler>();

if (buffHandler == null) return;

// 2. 从配表获取 Buff 配置 (假设 ID 为 1001)

int buffId = 1001;

BuffInfo originBuffInfo = 

GameTables.Instance.Tables.

TbBuffInfo.Get(buffId);

if (originBuffInfo == null) {

    Debug.LogError($"Buff ID 

    {buffId} 不存在");

    return;

}

// 3. 转换为运行时数据 (解析内部 JSON)

BuffInfo buffInfo = BuffConverter.

ConvertToBaseBuffModule(ref 

originBuffInfo);

// 4. 设置运行时上下文 (必须设置 

creator 和 target)

buffInfo.creator = 

myGameObject; // 施法者

buffInfo.target = 

targetGameObject; // 承受者

// 5. 施加 Buff

buffHandler.AddBuff(buffInfo);

5. 如何配置 (excel 表)

5.1 基础字段

  • id: Buff 唯一 ID。
  • buffData:
    • duration: 持续时间(秒)。
    • tickTime: 周期触发间隔(秒)。
    • maxStack: 最大叠加层数。
    • isForever: 是否永久(忽略 duration)。
    • buffUpdateTime: 叠加规则 (1: Add 叠加时间, 2: Replace 重置时间, 3: Keep 保持不变)。
    • buffRemoveStackUpdate: 移除规则 (1: Clear 直接清除, 2: Reduce 减少一层)。

5.2 逻辑字段 (Polymorphic JSON)

OnCreate, OnTick 等字段是 字符串 类型的,内容是 转义后的 JSON。 必须包含 "$type" 字段来指定对应的 C# 类名。

复制代码

6. 如何新增 Buff 代码

如果你需要一个新的 Buff 效果(例如:"无敌"),请按照以下步骤操作:

步骤 1: 创建 C# 类

在 Assets/HotUpdate/Buff/BaseBuffModule/ 目录下新建脚本 Invincible.cs。 类必须继承自 BaseBuffModule (注意是 partial 类),并放在 cfg.Buff 命名空间下。

cs 复制代码
using UnityEngine;

namespace cfg.Buff

{

    // 类名必须与配置中 $type 一致

    public partial class 

    Invincible : BaseBuffModule

    {

        // 如果配置中有额外参数,直接定

        义 public 字段,名称与 JSON 

        key 一致

        public float 

        extraEffectValue; 

        public override void Apply

        (BuffInfo buffInfo, 

        DamageInfo damageInfo = 

        null)

        {

            // 在这里编写具体逻辑

            if (buffInfo.target != 

            null)

            {

                Debug.Log($"

                {buffInfo.target.

                name} 进入无敌状态!

                额外参数: 

                {extraEffectValue}

                ");

                

                // 示例:获取目标身上

                的某个组件并修改状态

                // var health = 

                buffInfo.target.

                GetComponent<Health

                >();

                // health.

                SetInvincible

                (true);

            }

        }

        

        // 如果需要在 Buff 移除时执行

        逻辑(如取消无敌),

        // 可以在配置的 OnRemove 字段

        填入同样的类型 "{ \"$type\": 

        \"Invincible\" }"

        // 然后在 Apply 中根据上下文判

        断,或者写两个不同的类(如 

        InvincibleStart 和 

        InvincibleEnd)

        // *通常建议写两个不同的类,或

        者在 Apply 内部无法区分是 

        Create 还是 Remove 调用的,

        // 除非通过传入的额外参数区分,

        例如 { "$type": 

        "Invincible", "enable": 

        true }*

    }

}

注意:目前的架构中,Apply 方法本身不知道它是被 OnCreate 还是 OnRemove 调用的。

  • 如果开启和关闭逻辑不同,建议写两个类,例如 InvincibleOn 和 InvincibleOff。
  • 或者同一个类加一个 bool isEnable 字段,配置时分别填 true 和 false。

步骤 3: 重新生成代码 (如果涉及 Luban 结构变更)

如果是纯逻辑代码新增,通常不需要重新生成 Luban 代码。但如果 BaseBuffModule 的反序列化逻辑是生成的,可能需要确保新类能被识别(依赖于具体的反序列化实现,通常 SimpleJSON 配合反射或生成的解析器工作)。从现有代码看,BuffConverter 使用 BaseBuffModule.DeserializeBaseBuffModule,这通常是 Luban 生成的工厂方法。

  • 重要:如果 DeserializeBaseBuffModule 是自动生成的,你可能需要运行 Luban 生成脚本,或者手动在生成的 BaseBuffModule 分部类中注册你的新类型(取决于项目具体的 Luban 模板配置)。

7. 现有常用 Buff 模块参考

  • Shield: 给予护盾 (shieldCount, baseDamage 等参数)。
  • CasuingDamage: 造成伤害 (DamgePerHit)。
  • SpawnObjectBM: 生成特效/物体 (objPath, localPosition, delayDestroyTime)。
  • ChangeCharacterPropertyBM: 修改角色属性 (attackDamage, bulletSpeedPoints 等)。
  • PowerfulBuffs: 复合 Buff,一次性添加多个 ID 的 Buff (buffIdList)。

上面是我让AI写的说明,写的8成是对的,还是可以的,但是还是要做几处补充说明

因为excel表里不能配置类,所以需要新建xml配置,让luban自动生成分部类,放到代码里面去自己建立对应分部,补全另一半。

XML 复制代码
<module name="Buff">

    <!-- 抽象基类 -->
    <bean name="BaseBuffModule">
    </bean>

    <!-- 具体逻辑 Bean: 修改角色属性 -->
    <bean name="ChangeCharacterPropertyBM" parent="BaseBuffModule">
        <var name="shootCooldownPoints" type="float?" comment="射击间隔比率"/>
        <var name="reloadSpeedPoints" type="float?" comment="换弹速度比率"/>
        <var name="bulletSpeedPoints" type="float?" comment="子弹飞行速度点数"/>
        <var name="rowAmmo" type="int?" comment="弹药排数 加减 0"/>
        <var name="attackDamage" type="float?" comment="增加的攻击力"/>
        <var name="attackDamagePoints" type="float?" comment="增加的攻击力百分比"/>
        <var name="criticalRate" type="float?" comment="暴击率 直接加减"/>
        <var name="criticalDamageRate" type="float?" comment="暴击伤害百分比 直接加减"/>
        <var name="ultimateSkillCooldownPoints" type="float?" comment="终极技能减少时间倍率"/>
        <var name="ultimateSkillCooldownSeconds" type="float?" comment="终极技能减少的秒数"/>
        <var name="bulletPenetrationCount" type="int?" comment="子弹能穿透的次数"/>
    </bean>
	
	<!-- 修改怪物属性 -->
	<bean name="ChangeEnemyPropertyBM" parent="BaseBuffModule">
		<var name="duration" type="float?" comment="持续时间"/>
		<var name="attackDamage" type="float?" comment="直接增加或减少的攻击力"/>
		<var name="attackDamagePoints" type="float?" comment="攻击力点数"/>
		<var name="moveSpeedPoints" type="float?" comment="移动速度点数"/>
		<var name="Knockback" type="float?" comment="被击退距离"/>
	</bean>
	
	<!-- 里面塞多种buffId,触发时施加里面的buffId -->
	<!-- 例如生成炸弹并清楚所有敌人就先写两个buff,再把这两个buff填进来 -->
	<bean name="PowerfulBuffs" parent="BaseBuffModule">
		<var name="buffIdList" type="(list#sep=,),int"/>
	</bean>
	
	<!-- 造成伤害 放在OnCreate里就是DPH,放在OnTick里就是DPS -->
	<bean name="CasuingDamage" parent="BaseBuffModule">
		<var name="damgePerHit" type="float"/>
	</bean>
	
	<!--原清屏效果-->
	<bean name="ClearAllEnemies" parent="BaseBuffModule">
     	</bean>

    <!--子弹散射分裂效果-->
    <bean name="BulletScatterSplit" parent="BaseBuffModule">
        <var name="splitNumber" type="int" comment="分裂子弹数"/>
        <var name="bulletDamageRate" type="float" comment="分裂伤害系数数"/>
    </bean>
	
	
	<!-- 时间流速减缓 scaleRate:0.7,时间流速变为70% -->
	<bean name="SlowTime" parent="BaseBuffModule">
		<var name="scaleRate" type="float"/>
		<!-- buff表里面已经写了buff 的持续时间 这里不用再写了 只要在 OnRemove的时候还原就好了 -->
		<!-- var name="duration" type="float"/ -->
	</bean>
	
	<!-- 影分身效果 -->
	<bean name="ShadowClone" parent="BaseBuffModule">
		<!-- 这里生成的是个预制体,配表拉不到这个预制体所以需要持续时间后销毁 -->
		<var name="duration" type="float"/>
	</bean>
	
	<!-- 生成护盾 -->
	<bean name="Shield" parent="BaseBuffModule">
		<var name="shieldCount" type="int" comment="护盾抵挡次数 "/>
		<var name="baseDamage" type="float" comment="破碎时造成基础伤害"/>
		<var name="maxHealthDamageRate" type="float" comment="造成怪物生命上限伤害百分比"/>
	</bean>

    <!-- 生成某个物体,附带生成坐标 -->
    <bean name="SpawnObjectBM" parent="BaseBuffModule">
        <var name="objPath" type="string"/>
        <var name="localPosition" type="vector3"/>
		<var name="delayDestroyTime" type="float?"/>
		<var name="triggerBuffId" type="int?" comment="物体生成时可以给他一个id,当敌人经过时会触发这个id的buff效果"/>
    </bean>
	
	<!-- 生成某个物体会附着到目标身上,附带生成坐标 -->
    <bean name="SpawnObjWithTarget" parent="BaseBuffModule">
        <var name="objPath" type="string"/>
        <var name="offsetPos" type="vector3" 
		comment="物体在他身上的偏移位置"/>
		<var name="delayDestroyTime" type="float?"/>
    </bean>

    <!-- Buff 配置表 -->
    <bean name="BuffData">
        <var name="id" type="int"/>
        <var name="buffName" type="string"/>
        <var name="description" type="string"/>
        <var name="icon" type="string?"/>
        <var name="priority" type="int"/>
        <var name="maxStack" type="int"/>
        <!-- <var name="tags" type="(list#sep=,),string"/> -->
        
        <var name="isForever" type="bool"/>
        <var name="duration" type="float"/>
        <var name="tickTime" type="float"/>
        
        <var name="buffUpdateTime" type="Buff.BuffUpdateTimeEnum"/>
        <var name="buffRemoveStackUpdate" type="Buff.BuffRemoveStackUpdateEnum"/>
        
        <!-- 多态回调字段:直接使用多态类型 -->
        <var name="OnCreate" type="BaseBuffModule?"/>
        <var name="OnRemove" type="BaseBuffModule?"/>
        <var name="OnTick" type="BaseBuffModule?"/>
        <var name="OnHit" type="BaseBuffModule?"/>
        <var name="OnBeHurt" type="BaseBuffModule?"/>
        <var name="OnKill" type="BaseBuffModule?"/>
        <var name="OnBeKill" type="BaseBuffModule?"/>
    </bean>

</module>

这里定义了,luban在自动生成代码的时候就会生成对应的分部,但是生成的类使用的模板默认是readonly的,所以我在模板里面把readonly去掉了

因为在Luban的excel里面不能直接写类,所以后面有一个string类型的OnCreate等回调点,在代码里面,第一次调用的时候会将这个json解析成类回来一次

bash 复制代码
using Luban;
using SimpleJSON;
{{
    parent_def_type = __bean.parent_def_type
    export_fields = __bean.export_fields
    hierarchy_export_fields = __bean.hierarchy_export_fields
}}

{{namespace_with_grace_begin __namespace_with_top_module}}
{{~if __bean.comment != '' ~}}
/// <summary>
/// {{escape_comment __bean.comment}}
/// </summary>
{{~end~}}
{{~
func get_ref_name 
    ret (format_property_name __code_style $0.name) + '_Ref'
end

func get_index_var_name
    ret (format_property_name __code_style $0.name) + '_Index'
end

func generate_resolve_field_ref
    field = $0
    fieldName = format_property_name __code_style field.name
    refTable = get_ref_table field
    if can_generate_ref field
        tableName = format_property_name __code_style refTable.name
        if field.is_nullable
            ret (get_ref_name field) + ' = ' + fieldName + '!= null ? tables.' + tableName + '.GetOrDefault(' + (get_value_of_nullable_type field.ctype fieldName) + ') : null;'
        else
            ret (get_ref_name field) + ' = tables.' + tableName + '.GetOrDefault(' + fieldName + ');'
        end
    else if can_generate_collection_ref field
        collection_ref_table = get_collection_ref_table field
        tableName = format_property_name __code_style collection_ref_table.name
		if field.ctype.type_name == 'list' || field.ctype.type_name == 'set'
            line1 = (get_ref_name field) + ' = new ' + (declaring_collection_ref_name field.ctype) + '();' + '\n'
            line2 = 'foreach (var _v in ' + fieldName + ') { ' + (get_ref_name field) + '.Add(tables.' + tableName + '.GetOrDefault(_v)); }' + '\n'
            ret line1 + line2
        else if field.ctype.type_name == 'array'
            line1 = (get_ref_name field) + ' = new ' + (declaring_type_name collection_ref_table.value_ttype) + '[' + fieldName + '.Length];' + '\n'
            line2 = 'for (int _i = 0; _i < ' + fieldName + '.Length; _i++) { ' + (get_ref_name field) + '[_i] = tables.' + tableName + '.GetOrDefault(' + fieldName + '[_i]); }' + '\n'
            ret line1 + line2
		else if field.ctype.type_name == 'map'
            line1 = (get_ref_name field) + ' = new ' + (declaring_collection_ref_name field.ctype) + '();' + '\n'
			line2 = 'foreach (var kvp in ' + fieldName + ') { ' + (get_ref_name field) + '.Add(kvp.Key, tables.' + tableName + '.GetOrDefault(kvp.Value)); }' + '\n'
			ret line1 + line2
		else
			ret ''
		end
    else
        if (is_field_bean_need_resolve_ref field)
            ret fieldName + '?.ResolveRef(tables);'
        else if (is_field_array_like_need_resolve_ref field)
            ret 'foreach (var _e in ' + fieldName + ') { _e?.ResolveRef(tables); }'
        else if (is_field_map_need_resolve_ref field)
            ret 'foreach (var _e in ' + fieldName + '.Values) { _e?.ResolveRef(tables); }'
        else
            ret ''
        end
    end
end
~}}
{{~if __bean.is_value_type~}}
public partial struct {{__name}}
{{~else~}}
public {{class_modifier __bean}} partial class {{__name}} : {{if parent_def_type}}{{__bean.parent}}{{else}}Luban.BeanBase{{end}}
{{~end~}}
{
    public {{__name}}(JSONNode _buf) {{if parent_def_type}} : base(_buf) {{end}}
    {
        {{~ for field in export_fields
            fieldName = format_property_name __code_style field.name
         ~}}
        {{deserialize_field fieldName '_buf' field.name field.ctype}}
            {{~if can_generate_ref field~}}
        {{get_ref_name field}} = null;
            {{~end~}}
            {{~if has_index field~}}
        foreach(var _v in {{fieldName}})
        { 
            {{get_index_var_name field}}.Add(_v.{{format_property_name __code_style (get_index_field field).name}}, _v);
        }
            {{~end~}}
        {{~end~}}
    }

    {{~if !__bean.is_value_type~}}
    public {{__name}}() {{if parent_def_type}} : base() {{end}}
    {
    }
    {{~end~}}

    public {{__name}}({{__name}} other) {{if parent_def_type}} : base(other) {{end}}
    {
        {{~ for field in export_fields
            fieldName = format_property_name __code_style field.name
         ~}}
        this.{{fieldName}} = other.{{fieldName}};
        {{~if can_generate_ref field~}}
        this.{{get_ref_name field}} = other.{{get_ref_name field}};
        {{~end~}}
        {{~if has_index field~}}
        foreach(var _v in other.{{get_index_var_name field}})
        {
            this.{{get_index_var_name field}}.Add(_v.Key, _v.Value);
        }
        {{~end~}}
        {{~end~}}
    }

    public static {{__name}} Deserialize{{__name}}(JSONNode _buf)
    {
    {{~if __bean.is_abstract_type~}}
        switch ((string)_buf["$type"])
        {
        {{~for child in __bean.hierarchy_not_abstract_children~}}
            case "{{impl_data_type child __bean}}": return new {{child.full_name}}(_buf);
        {{~end~}}
            default: throw new SerializationException();
        }
    {{~else~}}
        return new {{__bean.full_name}}(_buf);
    {{~end~}}
    }

    {{~ for field in export_fields ~}}
{{~if field.comment != '' ~}}
    /// <summary>
    /// {{escape_comment field.comment}}
    /// </summary>
{{~end~}}
    public {{declaring_type_name field.ctype}} {{format_property_name __code_style field.name}};
    {{~if can_generate_ref field~}}
    public {{declaring_type_name (get_ref_type field)}} {{get_ref_name field}};
    {{~else if can_generate_collection_ref field~}}
    public {{declaring_collection_ref_name field.ctype}} {{get_ref_name field}};
    {{~end~}}
   {{~if has_index field
        indexMapType = get_index_map_type field
   ~}}
    public readonly {{declaring_type_name indexMapType}} {{get_index_var_name field}} = new {{declaring_type_name indexMapType}}();
   {{~end~}}
   {{~end~}}
   
{{~if !__bean.is_abstract_type && !__bean.is_value_type~}}
    public const int __ID__ = {{__bean.id}};
    public override int GetTypeId() => __ID__;
{{~end~}}

    public {{method_modifier __bean}} void ResolveRef({{__manager_name}} tables)
    {
        {{~if parent_def_type~}}
        base.ResolveRef(tables);
        {{~end~}}
        {{~for field in export_fields~}}
            {{~ line = generate_resolve_field_ref field ~}}
                {{~ if line != ''~}}
        {{line}}
                {{~end~}}
        {{~end~}}
    }

    public override string ToString()
    {
        return "{{full_name}}{ "
    {{~for field in hierarchy_export_fields ~}}
        + "{{format_field_name __code_style field.name}}:" + {{to_pretty_string (format_property_name __code_style field.name) field.ctype}} + ","
    {{~end~}}
        + "}";
    }
}
{{namespace_with_grace_end __namespace_with_top_module}}
相关推荐
ellis1970几秒前
Unity特殊目录小结
unity
专注&突破1 分钟前
DeepAgents 的 Backend详解
数据库
星火开发设计9 分钟前
序列式容器:list 双向链表的特性与用法
开发语言·前端·数据结构·数据库·c++·链表·list
Zzz 小生14 分钟前
LangChain Messages:消息使用完全指南
数据库·windows·microsoft
寂寞旅行9 小时前
向量数据库Milvus的使用
数据库·milvus
闻哥9 小时前
Redis事务详解
java·数据库·spring boot·redis·缓存·面试
道亦无名10 小时前
aiPbMgrSendAck
java·网络·数据库
面向对象World12 小时前
正点原子Mini Linux 4.3寸800x480触摸屏gt115x驱动
linux·服务器·数据库
dinga1985102613 小时前
mysql之联合索引
数据库·mysql
微风中的麦穗13 小时前
【SQL Server 2019】企业级数据库系统—数据库SQL Server 2019保姆级详细图文下载安装完全指南
大数据·数据库·sqlserver·云计算·个人开发·运维必备·sqlserver2019