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}}
相关推荐
BD_Marathon2 小时前
动态SQL(六)foreach标签2
数据库·sql
小豪GO!2 小时前
数据库-八股
数据库
IT大白2 小时前
1、一条SQL是如何执行的
数据库·sql
北京地铁1号线2 小时前
2.2 向量数据库
数据库·elasticsearch·milvus·faiss·向量数据库·hnsw
悟能不能悟2 小时前
查找oracle,存储过程包含某个单词的存储过程名称
数据库·oracle
马克学长2 小时前
SSM学生综合考评系统b8vlm(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm 框架·学生综合考评系统·高校学生管理、
独自破碎E3 小时前
MySQL中如何进行SQL调优?
数据库·sql·mysql
laplace01233 小时前
第八章 agent记忆与检索 下
数据库·人工智能·笔记·agent·rag
MyselfO(∩_∩)O3 小时前
1148. 文章浏览 I
数据库