# Conflicts:
#	.obsidian/workspace.json
This commit is contained in:
zzzisfunnyboy
2025-06-16 22:00:16 +08:00
18 changed files with 5409 additions and 55 deletions

3
.obsidian/app.json vendored
View File

@@ -1,5 +1,6 @@
{
"promptDelete": false,
"attachmentFolderPath": "渲染/软渲染/图片",
"alwaysUpdateLinks": true
"alwaysUpdateLinks": true,
"showLineNumber": false
}

View File

@@ -1,5 +1,8 @@
### GAS优先
- [ ] [焦虑、内耗、爱破防,真的是你的锅吗?硬核心理学让你从此掌控自我!-我要等到什么时候-稍后再看-哔哩哔哩视频](https://www.bilibili.com/list/watchlater?oid=114079968532107&bvid=BV1YM9gYdECb&spm_id_from=333.1007.top_right_bar_window_view_later.content.click)
- [ ] [动作游戏框架05.扩展Timeline使用组合而非继承来扩展Timeline的基础功能-我要等到什么时候-稍后再看-哔哩哔哩视频](https://www.bilibili.com/list/watchlater?oid=114522652147889&bvid=BV1HKJAzyEZ5&spm_id_from=333.1007.top_right_bar_window_view_later.content.click)
- [ ] https://zhuanlan.zhihu.com/p/28352150798# Unity-SM节点式动画技能编辑器
- [ ] https://zhuanlan.zhihu.com/p/28352150798# Unity-SM节点式动画技能编辑器
- [ ] [[Log插件]]
- [ ] [[UE-CORE-002 TickTaskManager]]

View File

@@ -1,40 +0,0 @@
> [!important] 优先级 **1**
- [x] 添加RoomComponents
- [ ] IExtension
- [ ] 生成怪物测试
- [ ] id不再自增长
- [x] 碰撞盒和标志需要添加自定义功能
- [ ] 地图地板图片导入 自动修改图片大小
- [ ] 添加白膜地图
- 创建100*100的地图宽高跟随Rect大小变化
- [ ] 将RoomConfig改到==关卡表==里
RoomConfig的内容为SpecialRoom
- [ ] 碰撞盒需要添加旋转
> [!note] 优先级 **2**
- [ ] 枚举改为英文名
- [ ] 瓦片地图
- [ ] 优化重构
[[需要做的优化]]
> [!note] 优先级 **3**
- [ ] 提取单个地块编辑
> [!note] 优先级 **4**
- [ ] 过关门动画优化
- [ ] 地图大背景优化移动方案

View File

@@ -0,0 +1,31 @@
> # 此版本
- [x] 皮肤导入
- [x] 上海限定 C:\Users\1\Desktop\cache\57015
- 游戏中
- [x] C:\Users\1\Desktop\cache\红/蓝/绿皮肤7.0
- [x] 胳膊导入
- 结算
- [x] C:\Users\1\Desktop\cache\角色胜利结算
- 卡面
- [x] C:\Users\1\Desktop\cache\第三弹裁切输出
- UI名字
- [x] C:\Users\1\Desktop\cache\皮肤
- [x] 枪械宠物UI导入
- [ ] 测试网络点数 开机检测
- [x] 地图编辑器 [[需要支持的地图功能]]
- [x] Boss立绘
- [x] C:\Users\1\Desktop\cache\雷鸟血条UI
- [ ] 积分系统
- [ ]
---
> ## 下个版本
- [ ] 网络状态3\4
[[1Project/宠物宇宙/硬件交互/业务插件RTwoGameBusinessTCP接口设计说明]]

View File

@@ -1,13 +0,0 @@
- [ ] 皮肤导入
- [x] 上海限定 C:\Users\1\Desktop\cache\57015
- 游戏中
- [x] C:\Users\1\Desktop\cache\红/蓝/绿皮肤7.0
- [ ] 胳膊导入
- 结算
- [x] C:\Users\1\Desktop\cache\角色胜利结算
- 卡面
- [x] C:\Users\1\Desktop\cache\第三弹裁切输出
- UI名字
- [ ] C:\Users\1\Desktop\cache\皮肤

View File

@@ -0,0 +1,153 @@
---
tags:
- Buff
---
source:[(2 封私信 / 4 条消息) 如何实现一个强大的MMO技能系统——BUFF - 知乎](https://zhuanlan.zhihu.com/p/150812545?utm_psn=1894773352150832417)
---
## 前言
Buff模块可以说是技能中最核心又最复杂的系统了。一个优秀的[Buff系统](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=Buff%E7%B3%BB%E7%BB%9F&zhida_source=entity)能够让策划的创意得到最大限度的发挥大幅增强游戏的战斗深度和可玩性并且同时也能让开发者轻易的扩展维护支持更多的效果和功能。本章将为你详细讲述一个强大的Buff系统是如何实现的。长文预警
## 正文
### 第一节Buff定义
首先我们将Buff系统分为三个层次具体继承关系如下
![](https://picx.zhimg.com/v2-fbac31f1aed84bb08ba0df09f73b6117_1440w.jpg)
**Buff**所有Buff的基类包含各类成员函数和基本接口。
**[Modifier](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=Modifier&zhida_source=entity)**继承于Buff代表这个Buff是一个修改器它可以用来修改当前目标的各种属性状态等等。抽象Modifier这个类的目的是出于性能优化的考虑。因为当Buff修改角色的属性或者状态时会导致重新计算角色的动态属性 而在游戏中我们很多的Buff并不需要修改角色的属性状态仅仅用来提供一段逻辑。那么如果它是一个Buff不是Modifier就不需要重新计算角色的动态属性。
**[MotionModifier](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=MotionModifier&zhida_source=entity)**继承于Modifier代表此类Buff提供修改玩家运动效果的功能。因为牵涉到与运动组件的交互所以抽象出一个新的类。
Buff类层次结构划分了之后那么Buff需要包含那些成员数据呢
我们提供BuffTypeId(Buff类型Id) **Caster**Buff施加者ParentBuff当前挂载的目标, **Ability**(Buff由哪个技能创建)BuffLayer层数, BuffLevel等级BuffDuration时长**BuffTagBuffImmuneTag免疫BuffTag**以及**Context**(Buff创建时的一些相关上下文数据)等等。
在这里我将说明一下CasterAbility以及Context这三个成员这也可能是我们Buff系统中一些独特的点。
**Caster代表Buff的施加者**它有可能为空也有可能不为空视具体构造时是否传Caster参数而定。但是Buff有一个配置项bNoCaster是否强制设置Caster为空。**如果bNoCaster = true。则Buff的Caster一定为空。**
为什么要有一个bNoCaster设置呢那是因为**我们的Caster不仅仅是一个成员项它还关系到Buff合并问题。**如果存在两个TypeId类型相同的Buff时候当他们的Caster相同才可以走合并流程Buff层数增加如果Caster不同则不能合并。**当策划有一些玩法需求可以多人给BOSS叠Buff时就可以配置Buff的bNoCaster=true**这样就不需要开发者在写代码添加Buff的时候小心翼翼的设置Caster参数为空了。另外还有几种情况也需要设置bNoCaster=true比如存在一个熔岩地图或者冰雪地图玩家每秒掉多少血量这个时候也可以配置bNoCaster=true。再比如说一些活动buff如双倍经验buff红名惩罚buff都可以由策划配置bNoCaster=true。类似于双倍经验还有红名Buff这种**所有需要存盘的Buff我们都需要设置bNoCaster=true。**也许会有人有疑问,这样能满足需求吗?完全可以,我会在最后的示例部分举出一个例子来解答这个疑问。
**Ability代表Buff是由哪个技能创建**它有可能为空也有可能不为空视具体构造时是否传Ability参数而定。通过Ability这个成员类型我们就将Buff与技能联系起来了我们能在Buff中取得技能的各种数据通过获取技能的数据然后由Buff来实现各种各样的技能效果。
**BuffTagBuffImmuneTag由策划配置(基于标记位标注这个Buff属于那些种类以及免疫哪些种类。**策划可以定义一些Tag如下
1. Metal = 1 << 1 金系
2. Wood = 1 << 2木系
3. Water = 1 << 3水系
4. Fire = 1 << 4火系
5. Earth = 1 << 5土系
当策划配置BuffTag为Meta | Wood时则代表这个Buff归属为金系和木系Buff如果策划配置BuffImmuneTag为Wood | Fire时则代表这个Buff可以免疫所有木系和火系Buff由于Tag的实际定义由策划控制策划可以根据他们的需求组合出各种各样的免疫效果我将在后面的示例里面描述一些基于Tag和ImmuneTag用法的例子来让读者体会Tag和ImmuneTag者两个概念抽象的简洁之美
**Context代表Buff创建时候的一些上下文数据**它是一个不确定的项通过外部传入各种自定义的数据然后在Buff逻辑中使用这些自定义数据
### 第二节Buff执行流程
在Buff从创建到销毁的过程中我们划分为如下几个阶段
1. Buff创建前检查当前Buff是否可创建一般主要是检测目标身上是否存在免疫该Buff的相关Buff如果被免疫则不会创建该Buff
2. Buff在实例化之后生效之前还未加入到Buff容器中时会抛出一个**[OnBuffAwake](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffAwake&zhida_source=entity)**事件如果存在某种Buff的效果是受到负面效果时驱散当前所有负面效果并给自己加一个护盾那么这个时候就需要监听BuffAwake事件了此时会给自己加护盾并且把所有负面Buff驱散。**这意味着一个Buff可能还未生效之前即销毁了小心Buff的生命周期)。**
3. 当Buff生效时加入到Buff容器后我们提供给策划一个抽象接口**[OnBuffStart](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffStart&zhida_source=entity)**由策划配置具体效果
4. 当Buff添加时存在相同类型且Caster相等的时候Buff执行刷新流程更新Buff层数等级持续时间等数据)。我们提供给策划一个抽象接口**[OnBuffRefresh](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffRefresh&zhida_source=entity)**由策划配置具体效果
5. 当Buff销毁前还未从Buff容器中移除我们提供给策划一个抽象接口**[OnBuffRemove](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffRemove&zhida_source=entity)**由策划配置具体效果
6. 当Buff销毁后已从Buff容器中移除我们提供给策划一个抽象接口**[OnBuffDestroy](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnBuffDestroy&zhida_source=entity)**由策划配置具体效果
7. Buff还可以创建定时器以触发间隔持续效果通过策划配置时调用**StartIntervalThink**操作提供**OnIntervalThink**抽象接口供策划配置具体效果
8. Buff还可以通过请求改变运动来触发相关效果通过策划配置时调用**ApplyMotion**操作提供**[OnMotionUpdate](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnMotionUpdate&zhida_source=entity)****[OnMotionInterrupt](https://zhida.zhihu.com/search?content_id=121646042&content_type=Article&match_order=1&q=OnMotionInterrupt&zhida_source=entity)**接口供策划配置具体效果
Buff由于其有着生命周期可控低耦合通过监听事件修改逻辑高内聚易于扩展的特性因此通过使用Buff来管理逻辑的话不仅方便处理各种复杂的行为同时还能有效的减少开发者的维护难度
例如延迟触发伤害是游戏中非常常见的需求在一些开发者的设计中就是直接给角色挂个定时器触发伤害简单的游戏里这样做没什么大问题但是如果技能逻辑稍微复杂点这样就会带来很多问题例如某天策划提出需求如果受到控制效果时需要取消该延迟伤害此时你怎么办直接干掉timer结果策划过了两天又提出了个新的需求还是受到控制效果时需要这个延迟伤害立即触发你又怎么办再又比如说当角色受到伤害超过1000点时这个延迟伤害立即触发你又该怎么做
这里就体现出Buff的方便之处了我们可以直接添加一个持续时间为N秒的BuffBuff销毁时触发伤害如果需求变更为受到控制时取消伤害那么我们就在Buff中检查当前是否包含有Tag为Control的Buff如果有则设置Buff.bTriggerDamage=false同时自我销毁。然后在BuffDestroy触发的时候检查是否触发伤害如果bTriggerDamage为false则不触发伤害。同理当需求为Buff监听伤害超过1000点伤害立即触发时我们只需要通过Buff监听OnTakeDamage事件检查当前受到的伤害值是否大于1000点如果是则销毁Buff此时立即触发BuffDestroy并执行伤害效果。
从上面的例子我们可以看出整个控制逻辑都是在Buff内部完成的不需要各种手动开启/取消定时器只需要Buff扩展下逻辑检查即可具有非常好的扩展性和高内聚性
### 第三节Buff修改状态ModifyState)
Buff可以通过修改状态去影响角色行为逻辑以下列举一些最常见的状态
1. Stun眩晕状态——目标不再响应任何操控
2. Root缠绕又称定身——目标不响应移动请求但是可以执行某些操作如施放某些技能
3. Silence 沉默——目标禁止施放技能
4. Invincible (无敌——几乎不受到所有的伤害和效果影响
5. Invisible (隐身——不可被其他人看见
这些状态是高度凝练的精华抽象到极致的代表。**非常多的游戏效果实际上都是这几种状态+运动+动画的组合。**这里很多开发者都会有一个**设计误区**就是**把Buff的状态跟运动和动画耦合在一块**比如眩晕状态一定就是播个眩晕动画然后击退状态就是击退位移+击退动画这样最后导致的问题就是状态膨胀而且各种逻辑耦合Bug频出最后维护成本大大提高
以Stun为例很多人第一眼看过去就觉得它是个Debuff是个敌人给我方加的控制Buff实际上并非如此Stun可以用到的地方非常多例如有个技能是野蛮冲撞释放后2秒内向前移动10米并将敌人推开那这个Buff的实现就是技能Spell的时候给角色加个Buff这个Buff会有个Stun状态同时带位移突进效果挂上这个Buff后技能施放后角色2秒内就不会响应角色按键移动和释放其他技能的请求了同时往前突进的效果由Buff控制将来处理各种位移打断效果也很方便 再比如说有个技能叫寒冰屏障你被一道寒冰屏障所笼罩在十秒内不会受到任何物理和法术伤害但这期间无法移动攻击或施法那这个技能的实现也很简单就是一个十秒的Buff同时添加了眩晕和无敌这两个状态如果还需要每秒回血则StartIntervalThink(interval)然后OnIntervalThink的时候Heal当前角色即可
除了各类战斗效果之外我们的Buff甚至可以扩展到一些其他场景比如说打BOSS前有个播过场动画的需求此时策划希望隐藏Boss和玩家的血条和姓名那么此时我们完全可以做个Buff这个Buff扩展个状态HideHpBar当有这个状态时即隐藏血条和名字就行了而且我们还可以让这个Buff加上无敌状态毕竟播过场动画的时候我们不希望玩家或者BOSS真的受到什么伤害
总而言之Buff状态除了上面提到几种高度凝练抽象的状态外我们还可以根据具体游戏的需求去扩展各种特殊状态以满足策划的需求同时方便开发者管理逻辑
### 第四节Buff修改属性(ModifyAttribute)
在游戏中Buff的添加与移除是一个频繁的过程而玩家的属性来源有很多如等级装备成就任务时装等等各种各样的来源相比于Buff这些模块修改属性的频率要远低于Buff所以我们一般将玩家的属性划分为两层第一层时Core核心层第二层是External外部层)。Core层是玩家各个其他模块的属性总和而External层则是Buff修改属性的总和两者相加既为玩家的实时属性
### 第五节Buff修改运动(ModifyMotion)
现在的MMO中为了增加动作表现力经常会有很多位移效果如突进翻滚千斤坠击退击飞拖拽吸引等等那么这些效果该如何实现呢而且有时候会遇到各种复杂的运动打断效果比如击飞时不能被击退击飞过程又能被冰冻效果定住然后又有破冰技能击退冰冻物体并解除冰冻效果面对这些复杂的情况我们该如何设计呢
在我们的系统中运动都是统一通过MovementComponent来管理因此通过使用MotionModifier来与MovementComponent交互MovementComponent中有一个CustomMotion用来具体实现各种运动位移具体运动实现相关细节我们将在后面的运动章节讲述
在MotionModifier中**我们会提供一个接口ApplyMotion(motionTypeIdpriority, forceInterrupt)来向运动组件请求运动效果。**同时通过设置回调UpdateBeforeMovement和UpdateAfterMovement来触发运动前和运动后的Buff效果下面我们初步介绍下ApplyMotion函数的三个参数
- motionTypeId运动类型id配置项包含运动位移参数及相关数据
- priority运动优先级每个运动都有优先级低优先级不能打断高优先级
- forceInterrrupt是否忽略优先级强制打断当前的Motion
通过这三个参数我们就能实现各类打断需求了
比如说击退的运动优先级是100击飞的运动优先级是200那么在击飞过程中施加击退Buff调用ApplyMotion的时候会返回false这时可以销毁掉这个击退Buff即击飞时无法击退如果击飞时被冰冻且冻在半空中停止不动那么我们就需要设计一个静止Buff运动优先级是300作用效果是速度设置为0不受重力影响同时修改Stun状态并挂载冰冻特效当破冰技消除冰冻效果时则设置破冰Buff的位移效果为击退设置运动优先级为100forceInterrupt为true此时ApplyMotion强制打断运动冰冻Buff会触发OnMotionInterrupt回调在此接口中冰冻Buff自我销毁即可
**Buff修改运动仅代表修改运动轨迹。**比如说击退仅仅只是以直线移动一段距离而击飞是以曲线移动一段距离同理**轻功的翻滚突刺其实都与击退是相同的运动轨迹。**他们都是在一定的时间内以直线到达目标地点且都设置Stun状态它们不一样的地方其实仅仅只是动画层的表现的不同。(可能策划还会设置不同的Tag和ImmuneTag标记下
**我们要牢牢记住玩家看起来各种花哨的轻功击退击飞等位移效果实际上是State+Motion+Animation的组合。**掌握住了这一点我们就可以通过简单的组合实现各种丰富的效果了而不会被各种花哨的效果所迷惑以为他们都是不一样的效果导致最后设计出无比庞杂且难以维护的系统了
### 第六节Buff监听事件
Buff可以通过监听各类事件执行特定逻辑或者修改事件数据来实现各种效果
最常见的事件监听一般有
- **OnAbilityExecuted监听某个主动技能执行成功。**常用于被动技能Buff比如说角色施法时有10%概率获得30%的攻速提升那么我们通常是Buff-A监听OnAbilityExcuted事件然后10%概率添加Buff-BBuff-B的作用是修改玩家属性增加30%攻速
- **OnBeforeGiveDamageOnAfterGiveDamage监听我方给目标造成伤害时触发。**比如说对目标造成的伤害有10%概率无法被闪避那么这个效果我们就可以通过监听OnBeforeGiveDamage的流程来实现当执行伤害流程时在计算伤害前我们抛出一个事件eventevent里面有当前伤害数据Buff在调用OnBeforeGiveDamage(event)修改event.Damage.DamageFlag |= DamageFlag_NotMiss标注该伤害无法被闪避就行了又或者如果有一个需求是给目标造成伤害后有10%几率触发DOT伤害效果那么我们在OnAfterGiveDamage的时候取出event.Target并给这个目标加个DOT类Buff即可
- **OnBeforeTakeDamageOnAfterTakeDamage监听我方受到伤害时触发。**如护盾类Buff通常在OnBeforeTakeDamage的时候修改伤害数据又或者有某些Buff在受到伤害后可以触发各类效果就可以通过监听OnAfterTakeDamage事件来触发指定逻辑
- **OnBeforeDeadOnAfterDead监听我方死亡时触发。**如免疫致死效果可以通过监听OnBeforeDead事件修改角色当前的Hp>0从而让角色提前退出死亡流程以避免死亡。死亡后触发额外效果如爆炸或者召唤其他生物都可以通过监听OnAfterDead事件来执行。
- **OnKill事件监听我方击杀目标时触发。**如当击杀目标后获得治疗效果回复即可通过监听到Kill事件时给自己加一个HOT的Buff来实现。
开发者可以通过扩展各类事件列表让Buff通过监听对应事件就能执行任意逻辑。不需要与任何模块耦合只需要抛出事件监听事件执行逻辑即可获得Buff功能上的扩展。
## 总结
以上我们通过六个小节讲述了Buff系统主要模块的实现方法。通过这样的设计我们让Buff的深度和扩展性都能够得到了极大的提升几乎能实现各种各样的效果。足以让策划的创意得到最大限度的发挥。
## 示例
为了让读者便于直观理解,我会提出一些具体实现的例子以供参考:
-Buff互斥效果也很常见怎么做
-BuffTag和BuffImmuneTag可轻松实现。比如说火系Buff和水系Buff互斥。无论策划的需求是存在水系Buff的时候无法添加火系Buff还是存在水系Buff的时候添加火系Buff会驱散水系Buff都可以实现。第一种情况最简单水系Buff配置Tag 为Water的时候配置ImmuneTag为Fire。此时存在水系Buff的时候即可免疫火系Buff了。第二中情况也好办。配置BuffTag为Water。当OnBuffStart的时候调用驱散接口DispelByTag(Water)驱散掉所有火系Tag相关Buff即可。
-霸体效果怎么实现而且假如说存在破霸体效果又怎么实现而且Boss的霸体效果完全不受影响又怎么实现万一还存在特殊效果可以让Boss受到控制怎么办
-我们可以定义两个BuffTagWeakControl弱控制和StrongControl(强控制普通霸体效果通过Buff配置ImmuneTagWeakControl即可免疫控制效果。如果是破霸体效果我们给这个Buff的Tag标记StrongControl就行同时Boss的Buff配置ImmuneTag为WeakControl | StrongControl免疫弱控制和强控制就满足需求了。如果存在某个特殊的效果能让Boss受到控制效果的话那这个Buff的Tag不要标记WeakControl和StrongControl就行了这样它就无法被免疫掉了。看起来复杂的霸体破霸体效果实际实现就这么简单就这么清晰不需要引入任何新的系统。
-Buff存盘那块如何处理跟施法者相关的属性数据如施法者可以给目标添加一个强力的毒Buff具体伤害数值有施法者属性决定离线后依旧生效直到Buff时间结束才移除。
-这块我们的处理依旧很简单Buff依然设置bNoCaster=true。但是在Buff创建的Context里面我们设置Context.DamageValue为根据施法者属性计算出来的伤害数值。然后Buff持续造成伤害的时候直接取Context.DamageValue即可。至于说想要玩家离线再上线Caster离线再上线后毒的伤害数值还能实时修改的话这样的需求是不存在的如果一定要做当然也能做只是麻烦一点而且也没有必要。这样的需求一般仅仅存在测试的大脑中策划是不会有这样的玩法需求了。
-常见的基于指定地点延迟触发的AOE效果怎么实现当技能施法成功后就延迟触发不会被打断AOE效果。如果能被打断我们可以用引导类技能轻松实现
- 答我们将技能标记为可指定目标地点释放当技能Spell的时候我们先给自己加一个Buff这个Buff仅仅用于延迟效果当然可以有更多的可能性如监听到某种事件立即结束并触发AOE效果当Buff持续时间到了的时候在OnBuffDestroy的时候创建AOE效果Buff。这个AOE Buff会调用StartIntervalThink函数在OnIntervalThink的时候通过Buff:GetAbility():GetCastPosition()为基准位置检查周围的敌方单位是否在AOE半径内如果是则施加作用效果。

View File

@@ -0,0 +1,5 @@
[【全网首发】专注力与创造力的秘诀|大脑炎症与创伤修复|脑科学与大脑默认模式网络|本世纪最详尽脑网络_哔哩哔哩_bilibili](https://www.bilibili.com/video/BV1DdXLYNEGz/?spm_id_from=333.788.recommend_more_video.17&vd_source=c0e8ba1ae97ae182776824f6b7c45879)
评论摘要
大道至简: 第一步先把身体搞好优先级排序睡眠、饮食、运动。第二步减少干扰黄片、游戏、小说、电影能免则免。第三步专项训练冥想和正念。第四步才是术番茄工作法、todolist、心流理论之类的。

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,153 @@
source:[加Log就卡不加Log就瞎”——这个插件治好了我的精神内耗](https://mp.weixin.qq.com/s/Nii4fC15eKoNN8MOAzrmDQ)
---
#
- 1 现有日志打印情况
- 1 日志阻塞
- 2 从调试到生产,日志策略的抉择
- 2 问题出现原因
- 2.1 日志打印原理分析
- 2.2 log4j2 Disruptor 的初始化
- 2.3 队列满导致日志阻塞
- 2.4 产生的根本原因
- 3 应对方案
- 3.1 方案选择
- 3.2 技术选择
- 3.3 落地实现
- 4 总结
## 1 现有日志打印情况
日志作为软件工程实践中的重要基础设施在系统监控、异常诊断及行为追溯等关键环节发挥着不可替代的作用。Apache Log4j2作为当前主流的日志框架凭借其模块化架构和高度可扩展的特性为开发者提供了灵活的多维度日志管理方案。然而若未能深入理解其异步日志机制、缓冲区策略等核心原理或存在配置参数与业务场景匹配度不足等问题则可能导致日志I/O阻塞、内存资源过度消耗等负面效应甚至引发严重的服务性能瓶颈。因此在实际工程实践中需遵循科学合理的使用准则通过日志分级管理、输出格式优化、滚动策略定制等手段方能充分发挥其技术优势有效规避潜在风险。
### 1 日志阻塞
日志导致线程Block的问题相信你或许已经遇到过对此应该深有体会或许你还没遇到过但不代表没有问题只是可能还没有触发而已。常见的现象是出现大量的block线程查看jstack常常是下图现象![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHIR2NAfdDEgKBvNfWKcAd1jXHF2qRU1ibbNA8jIMaTs5hrlGibj02kiblg/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)
### 2 从调试到生产,日志策略的抉择
  在软件项目的全生命周期中,从开发阶段到生产环境的演进过程中,日志管理往往面临着微妙的平衡。开发阶段我们倾向于采用详尽的日志策略:业务接口的入参出参被完整记录,跨系统的调用链路被清晰标注,甚至非核心逻辑的辅助性信息也得以留存——这些详实的日志如同开发者的双目,为联调排障与功能验证提供了不可或缺的洞察。
  然而当服务迈向生产环境时,过度日志带来的问题便逐渐显现。冗余的调试信息不仅会影响系统性能,更可能淹没真正关键的业务轨迹。尽管我们尝试在上线前进行日志裁剪,但总存在令人踌躇的灰色地带:某些开发期辅助日志是否暗含未来的诊断价值?那些看似非核心的流程记录会否在某个异常场景下成为关键线索?这种取舍的困境,本质上反映了我们对系统可观测性与运行效能之间永续的权衡。
## 2 问题出现原因
### 2.1 日志打印原理分析
在我们使用的log4j的应用中采用的是异步日志配置简单的说明一下一条日志打印在log4j中的处理流程如下图所示![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHwWGYn7OQfa7tNica1TerbgEjIP0VNJnNbpGlKdAvCic5Lbk0sTCNElTw/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)  简单点来说,就是多线程通过 log4j2 的门面类进行日志的打印日志经过一系列的处理过滤包装后放入到Disruptor的环形 buffer 中,在服务器的消费端会单启一个线程进行这些日志的消费,最终放入到我们指定的文件中。
### 2.2 log4j2 Disruptor 的初始化
当LoggerContext启动时所有AsyncLoggerConfig会通过start()方法初始化其Disruptor![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHvaPOfupg1o0Wnb2HpqzUewncx7ibHiczTLJdCq6Na2yopKYxZ3thu14w/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)其中Disruptor 是一个环形 buffer官方做了很多的性能优化这里有兴趣的可以了解其实现原理这里不进行深入的讨论其中在我们的应用log4j.xml配置中没对RingBuffer进行自定义的配置使用的是默认的大小256K。
### 2.3 队列满导致日志阻塞
Disruptor 的 RingBuffer 是一个固定大小的环形队列,其发布逻辑:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHiav0RIpxia6HI61Y86ohwoyxYkic8Zsiaj6CygPU5PEibrVuEMC7kialgKWg/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)
队列满时的默认行为AsyncLoggerConfig.SynchronizeEnqueueWhenQueueFull=true此时会等待着消费出下一个可以生产的环形 buffer 槽;此时所有打印日志的线程会尝试获取全局锁。此时会阻塞线程,也就是我们上述堆栈中看到的异常。
### 2.4 产生的根本原因
  生产者速度 > 消费者速度:
AsyncAppender 的后台线程从队列中取出事件并交给实际 Appender如 FileAppender处理如果实际 Appender 的写入速度慢(如磁盘 I/O 高消费者线程无法及时清空队列导致队列积压。其实log4j消费时会调用多次 flush这些flush的调用根本在文件写入的 native 调用当这种native调用太多时系统写入不过来。
## 3 应对方案
### 3.1 方案选择
  上述问题情况解决,大致分成两个方向:生产者方向&消费者方向,具体行为如下图简述:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHWnueNI4rlREl0V3JSEtRDC1ibkpR4jhZ2XqMFbrsqiarajepynlCIvfA/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)在应对日志管理的挑战时除了调整日志队列容量等基础优化需警惕OOM风险更核心的问题在于如何平衡日志的详实性与系统稳定性。开发者往往陷入两难若详尽记录日志可能引发阻塞风险若过度精简则排查问题时如盲人摸象难溯根源。
为此,可考虑将日志划分为两类:
功能日志(必须):如埋点数据、核心流程记录,确保业务可观测性;
业务排查日志非必须如RPC入参/出参、调试断点等,按需动态启停;
  通过这种分层策略,既能在高并发场景下保障核心日志的稳定输出,又能灵活控制辅助日志的打印量,使系统整体具备更强的适应性与可控性。如此,我们既能从容应对生产环境的严苛要求,又能在需要时快速激活详尽的诊断信息,实现运维效率与系统性能的兼得。
### 3.2 技术选择
在日志打印的精细化控制中核心在于灵活性与精准度的平衡。传统的全局级别过滤如INFO/WARN/ERROR虽能粗放管理却难以适配复杂多变的业务场景。理想的方案应突破层级限制实现行级细粒度控制——无论是核心链路的关键节点还是特定业务场景的临时调试均可针对单行日志动态启停。
  这种设计赋予开发者更高的自主权:业务视角:按需捕获特定模块的完整上下文;链路视角:精准聚焦某次调用的全生命周期轨迹;应急场景:即时激活深层诊断日志,无需重启或改码。
  通过将控制粒度细化至代码行,我们既能维持生产环境的日志精简,又能随时按业务诉求“点亮”关键路径,使系统可观测性兼具严谨与弹性。
#### 3.2.1 区分必要日志和非必要日志打印:
 自定义封装日志打印的方式如下图所示:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHvb9FLG3BmhibttKEy83BOWs8t7gXOdibkPicJaV18MVzicLUKlsO7a1nVQ/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)
#### 3.2.2 如何对非必要日志进行行级别控制
1、自定义Appender中的filter
在实现行级日志控制时若需精确控制特定代码行如第133行的日志输出采用自定义Appender过滤机制是一种可行方案。其核心思路在于通过解析日志调用的堆栈信息动态判断当前行号是否符合预设的打印条件若不符合则直接过滤。
  然而,该方案存在若干固有局限: 堆栈解析的可靠性问题Lambda表达式中的日志调用往往难以准确获取行号信息即使通过堆栈缓存优化仍存在定位失准的风险性能损耗隐患频繁的堆栈遍历操作会引入不可忽视的性能开销在高并发场景下可能成为新的瓶颈分类管理缺失该机制难以与既有的日志分级体系必需/非必需日志)形成有机协同,增加了运维复杂度。
2、在打印日志前获取日志的行信息
  最简单的方式是人工的形式,在写日志的时候同时将日志的行信息写入进去,比如:![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHeZpkYq1kU9e6CAgUu57X9iblfAWCDURp2yZhDkJN2HEcicND4nIiciaTLA/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)这种方式在可扩展性和可观测性维度存在设计缺陷。
3、在编译的时候获取日志的行信息
我们想使用LogUtils.debug(()->log.info("业务日志"));这种方式但是我们不会在代码中明显的写入可以在代码编译期间将行信息获取到后使用字节码修改这行代码利用Java的重写。将它转变成 LogUtils.debug("类+行",()->log.info("业务日志")); 然后再这个方法执行中进行条件判断。
字节码技术选择:
![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHgVuiaHTK1MtAjYCibgrCVDZJ6oOyx4ac2R1O3CydMwX9fngib5diaWpnoQ/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)  在本次实现中需要更灵活的方式操作字节码,还要考虑性能的问题,以及对应用框架的支持, 我们选择ASM的形式。
4、如何随心控制开启和关闭
这里我们采用的是ider插件的方式有idea插件上报我们的对这行日志的控制行为如下图所示![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHNHrI1ibxujSAmoPy2AXYUx8PbdXEyDyl6cJWIJrOEvSaEWYNMcRerIw/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)
### 3.3 落地实现
#### 3.3.1 Maven编译插件
  目的:获取日志所在的类和行信息。
  运行时获取的方式:在 Logger 配置中启用 includeLocation代码从 LogEvent通过堆栈分析获取行号。这种方式存在很大的弊端堆栈跟踪生成开销很大每次调用 getStackTrace() 时JVM 需要遍历当前线程的调用栈,生成完整的堆栈信息,这是一个 同步且耗时 的操作(尤其在深调用链中),如果每秒有数万次日志调用,频繁生成堆栈跟踪可能导致 CPU 使用率飙升,直接影响吞吐量。
Maven的process-classes阶段获取通过Maven编译之后获取到字节码文件时对字节码文件进行修改存放日志的类和行信息。对运行期间无额外消耗。处理逻辑如下图所示![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHDvnxCOu4GPuag84E5dITibibdYwfVPKdRwMSIiby36Mb08YNX88CmAWLA/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)
#### 3.3.2 Idea插件
  目的:精准的控制某一行日志是否进行打印。
利用Idea的插件能力将我们对某一行日志的开启和关闭状态进行上报整个过程不阻塞主线程保持Idea操作流畅性。提供定时能力保障线上我们可以更灵活的控制日志是否打印的状态。处理逻辑如下图所示![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHSqkWttkMvjV3ELwy9OKhHwrsz1msFA18D5vaOFneT1AJ7QkjtRWIPQ/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)
#### 3.3.3 整体流程
使用方式通过在项目中使用上述Maven插件对项目进行编译部署使用Idea插件对目标日志的是否开启打印状态进行上报存储状态采用的Apollo的能力通过自定义打印工具类中对Apollo配置内容的分析进一步做判断逻辑最终将日志进行打印或者不打印。处理逻辑如下图所示![图片](https://mmbiz.qpic.cn/mmbiz_png/dHUzltsJpQsMHUpibOgSB9tj4iaVbseaZHU1ibLCk1ZVPspwevm1y1AAX5QKibQ1nXKic2YKA3RWoDlxzlD5ibym8vMw/640?wx_fmt=png&from=appmsg&tp=webp&wxfrom=5&wx_lazy=1)
## 4 总结
在分布式系统日益复杂的今天日志管理已从简单的信息记录演进为系统可观测性的核心支柱。本文揭示的日志阻塞与策略困境折射出现代化服务在稳定性与可维护性之间的深层博弈。通过剖析Log4j2异步日志机制的内在原理我们识别出队列积压导致线程阻塞的关键症结并由此展开对日志治理体系的深度重构。
本次优化方案突破传统日志分级思维的桎梏创新性地提出双轨制日志管理体系将日志划分为功能型与诊断型两类前者确保核心业务脉络的持续可见后者实现按需动态管控。通过编译期字节码增强技术我们实现代码行级别的精准控制配合IDE插件的可视化操作使开发人员能够像调试断点般自由启停日志输出。这种"外科手术式"的日志管理,既避免了传统方案"一刀切"的弊端,又赋予系统在高负载场景下的弹性适应能力。

View File

@@ -0,0 +1,22 @@
Source:[loxodon-framework/Loxodon.Framework.TextUGUI at master · vovgou/loxodon-framework · GitHub](https://github.com/vovgou/loxodon-framework/tree/master/Loxodon.Framework.TextUGUI)
---
单向绑定
``` csharp
[OnValueChanged(viewModel=>viewModel.hp, value=>string.Format("{0:D4}"))]
CustomText text;
public class CustomText
{
public Text text;
public void SetText(string str)
{
text.text = str;
}
}
```

View File

View File

@@ -0,0 +1,5 @@
Source:[Unity网络请求封装实战手把手教你打造一套可扩展的网络请求框架](https://mp.weixin.qq.com/s/p9qDdJqHj-7x58XAPNNabA)
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB