# CombatTest **Repository Path**: kyoa/CombatTest ## Basic Information - **Project Name**: CombatTest - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-28 - **Last Updated**: 2026-10-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # UE5.3 战斗系统技术测试 · 实现说明 > 引擎:UE 5.3.2 | 语言:C++(GAS) | 逻辑帧率:固定 60 FPS(1 帧 = 16.666 ms) > 所有战斗时序以**帧**为单位,全部来自 DataTable,工程内无硬编码时序常量。 --- ## 1. 快速开始 | 操作 | 按键 | |---|---| | 移动 / 视角 | W A S D / 鼠标 | | 轻攻击(三段连招) | **F** | | 重攻击 | **R** | | 闪避(翻滚) | **G** | | 弹反 | **H** | 场景内有一个敌人 NPC,会自动追击并攻击。启动时会校验配置表,窗口帧号异常会打印警告。 ### 调试开关 | 控制台命令 | 作用 | |---|---| | `combat.Log 1` | 打开战斗日志(默认 **0 关闭**)。帧号、连招接续、冻结窗口、AI 感知都在这里 | | `combat.DisableEnemyAI 1` | 暂停所有敌人行为树,方便单独测试玩家 | | `combat.DebugFrameClock 1` | 打印逻辑帧间隔,用于验证 60 FPS | 战斗代码里所有 `UE_LOG` 都换成了 `COMBAT_LOG` 宏,受 `combat.Log` 控制; 数据生成命令台(`-run=CombatGenerateData`)仍直接用 `UE_LOG`,构建输出始终可见。 --- ## 2. 架构总览 ``` ┌──────────────────────────────┐ │ UCombatFrameClockSubsystem │ 固定 60Hz 逻辑时钟 │ (TickableWorldSubsystem) │ World Tick → 累加器 → 派发帧 └──────────────┬───────────────┘ │ OnFrame(GlobalFrame) ┌──────────────┴───────────────┐ │ UCombatActorFrameComponent │ 每个角色一份 │ · LocalFrame 帧游标 │ │ · 冻结栈(顿帧) │ └──────────────┬───────────────┘ │ OnActorCombatFrame(Frame) ┌──────────────┴───────────────┐ │ UCombatGameplayAbility │ 所有战斗技能基类 │ · OnCombatFrame(int32) │ 帧驱动,替代 Tick/Timer │ · ApplyCombatTag / 阶段白名单 │ 状态全部走 GameplayTag └──────────────┬───────────────┘ ┌────────────────────┼────────────────────┐ │ │ │ CombatAbility_ CombatAbility_ CombatAbility_ MeleeAttack Dodge Parry HitReact / EnemyAttack / DeathState ``` ### 核心类职责 | 类 | 职责 | |---|---| | `UCombatFrameClockSubsystem` | 固定 60Hz 逻辑时钟,把不可控的 DeltaTime 转成确定帧号 | | `UCombatActorFrameComponent` | 每角色帧游标 + 顿帧冻结栈(可嵌套,FGuid 句柄) | | `UCombatGameplayAbility` | 技能基类:帧绑定、阶段标签、蒙太奇拟合播放 | | `UCombatInputBufferComponent` | FIFO 输入缓冲,深度与有效期来自全局表 | | `UCombatDataSubsystem` | 4 张表索引 + 连招图查询 + 启动校验 | | `UCombatSettings` | DeveloperSettings,配置表软引用 | | `UCombatGenerateDataCommandlet` | 一键生成/校验/诊断数据表与动画资产 | --- ## 2.5 镜头 俯视角(等距)镜头,风格接近《恶意不息》,由代码配置而非蓝图搭建: | 参数 | 默认 | 说明 | |---|---|---| | `CameraPitch` | -50° | 俯视角度 | | `CameraYaw` | 45° | 固定水平朝向 | | `CameraDistance` | 1150 | 镜头距离 | | `CameraHeightOffset` | 80 | 注视点抬高 | | `CameraLagSpeed` | 6 | 跟随延迟 | 关键设计: - 弹簧臂使用**绝对旋转**(`SetUsingAbsoluteRotation(true)`),角色转身时视角完全不转 - 关闭镜头碰撞检测,避免贴墙时镜头突然拉近 - **移动改为相对镜头**(W 永远是屏幕上方),而不是相对控制器朝向 - **鼠标视角输入被禁用**,攻击时角色转向"最后移动的方向"而不是镜头方向 ```cpp // 固定视角下控制器朝向没有意义,攻击朝玩家来的方向 float GetDesiredFacingYaw() const; // LastInputVector 的朝向,无输入则保持 ``` ## 3. 帧时序系统 ### 3.1 为什么不用 Timer 或 Tick Delta - **Timer** 精度受帧率影响:30 FPS 下 6 帧 = 200 ms,60 FPS 下 = 100 ms,判定窗口长度会漂移 - **DeltaTime 累加** 在掉帧时会把多帧逻辑挤进一次 Tick,可能出现“一次 Tick 打出三段连招” 本实现把逻辑时间与渲染时间解耦: ``` World Tick(DeltaTime) → Accumulator += DeltaTime → while (Accumulator >= 1/60) { AdvanceFrame(); Accumulator -= 1/60; } ``` 掉帧时逻辑帧会追赶(上限 `MaxCatchUpFrames`,避免长时间卡顿后爆帧),但每帧语义始终是 1/60 秒。 ### 3.2 顿帧(Hit Stop) 命中时攻击方与受击方**各自冻结 N 帧**,通过冻结栈实现: ``` RequestFreeze(Frames, OnExpired) → FGuid ├─ 冻结栈压入 ├─ GlobalAnimRateScale = 0 + 禁止位移 ├─ N 帧后弹出并触发 OnExpired(延迟的击退/硬直在这里执行) └─ 栈空 → 恢复动画与移动 ``` 冻结的是**该角色自己的帧游标**,所以两个角色可以有不同冻结时长而互不影响——世界时钟照常推进。 ### 3.3 逻辑帧与动画帧的对齐 逻辑帧与动画进度通过**拟合播放**严格线性对应: ``` PlayRate = 切片长度 / (总帧数 / 60) ``` 因此 11/7/18 与蒙太奇进度是固定映射:第 1 帧 = 0%,第 11 帧 = 前摇结束,第 18 帧 = 判定结束。 > 题目提到“由动画通知标记帧区间”。本项目选择用**帧计数**而非 AnimNotify 驱动逻辑: > AnimNotify 在掉帧时可能被跳过或合并,而帧计数在任何帧率下都严格等于配置值。 > 两者可用同一张表对齐——表中的窗口帧号即通知应放置的帧号。 --- ## 4. 数据驱动 ### 4.1 四张表 | 表 | 内容 | 主要字段 | |---|---|---| | `DT_AttackConfig` | 攻击段数据 | 前摇/判定/后摇、连招窗口、取消窗口、伤害、顿帧、击退、硬直、霸体、转向速度、蒙太奇切片、受击反应 | | `DT_DefenseConfig` | 闪避 / 弹反 | 帧结构、无敌窗口、成功奖励、位移、蒙太奇切片 | | `DT_ComboGraph` | 连招图 | (源攻击, 输入Tag, 帧区间) → 目标攻击 | | `DT_GlobalConfig` | 全局参数 | 逻辑帧率、缓冲深度、缓冲有效期 | ### 4.2 当前参数与题目给定值对照 | 招式 | 题目给定 | 实际使用 | 偏差原因 | |---|---|---|---| | 轻攻击 1 | 6 / 4 / 10(20 帧) | **11 / 7 / 18(36 帧)** | 20 帧 = 0.33 s,60 FPS 下动作无法辨识、输入窗口过窄 | | 轻攻击 2 | 6 / 4 / 12 | **10 / 7 / 19(36 帧)** | 同上 | | 轻攻击 3 | 8 / 5 / 16 | **10 / 6 / 20(36 帧)** | 同上 | | 重攻击 | 15 / 6 / 20(41 帧) | **18 / 7 / 23(48 帧)** | 同上 | **前摇 : 判定 : 后摇 ≈ 6 : 4 : 10 的比例全部保留**,整体放大到与动画长度匹配 (帧数 = 动画时长 × 60,因此播放倍速严格 1.0)。 缓冲深度 2、缓冲有效期 15 帧、顿帧、伤害、击退等其余数值均按题目要求实现。 ### 4.3 连招窗口 窗口从**总帧数的 60% 处**开启(如轻攻击 1 为 22..36)。更早开窗会让每段攻击只播一半就被截断, 观感像“动作被加速”;更晚则连招拖沓。按比例而非固定帧数,是因为徒手和双手武器的攻击长度差了将近一倍。 ### 4.4 数值都放在哪里 代码里没有“魔法数字”,每个数值都有明确归属: | 类别 | 位置 | 例子 | |---|---|---| | 战斗时序 / 伤害 / 连招 | **DataTable** | `DT_AttackConfig`、`DT_DefenseConfig`、`DT_ComboGraph` | | 全局参数 / 表现手感 | **DataTable**(`DT_GlobalConfig`) | 逻辑帧率、缓冲深度、镜头抖动幅度与频率 | | AI / 武器调参 | **蓝图属性**(`EditDefaultsOnly`) | 视野半径、攻击距离、寻路重算间隔、蒙太奇停止比例 | | 算法常量 | **具名 `constexpr`** | `TraceHeightFraction`、`MidpointWeight`、`TwoPi` | 算法常量也用具名常量而不是字面量: ```cpp namespace { /** The sweep is centred at this fraction of the capsule half height, roughly chest level. */ constexpr float TraceHeightFraction = 0.5f; } ``` 命名本身就是文档:读代码的人不需要猜 `0.5f` 是什么,注释说明了为什么是这个值。 --- ## 5. 机制实现 ### 5.1 输入缓冲 ``` 按下 F ├─ PushInput(Input.Attack.Light) │ ├─ 先剪除过期条目(当前帧 - 入队帧 >= 15) │ ├─ 队列已满 2 条 → 丢弃(日志 Buffer full) │ └─ 入队并记录帧号 └─ TryActivateAbilitiesByTag ├─ 空闲 → 立即激活,消费该条作为起手式 └─ 攻击中 → 激活被 State.Attack 阻塞,条目留在队列 ``` **每条输入只被消费一次**:接续成功 → `ConsumeOldest`;起手成功 → 同样消费。 这从结构上保证“不得批量释放”。 ### 5.2 连招接续 ``` if (IsComboWindowOpen(Frame)) → PeekOldest() 取最老一条 → 查 DT_ComboGraph:(当前攻击, 输入Tag) 有边且 Frame 在边的区间内? → 有 → ConsumeOldest + StartAttack(下一段) → 无 → 保留在队列里等过期(不会被误消费) ``` **接续不结束技能**,而是同一实例换数据、重置帧游标、重播蒙太奇。 这样避免“结束→重新激活”时被自己身上的 `State.Attack` 阻塞,也没有 GAS 激活开销。 ### 5.3 命中与结算顺序 ``` 判定帧 → BoxTrace 扫描 → 命中 ① 记录已命中(同一段攻击对同一目标只结算一次) ② 施加伤害 GE(SetByCaller Data.Damage,负值扣血) ③ 双方 RequestFreeze(HitStopFrames) ④ 冻结到期回调:先施加击退(按帧均分),再发送受击事件 → 硬直 ``` 顺序刻意如此:**顿帧 → 击退 → 硬直**。击退必须在冻结结束后开始,否则角色会在冻结期间原地滑走。 ### 5.4 闪避 - 帧结构 **6 / 7~34 / 22(56 帧)**,由动画长度反推、按题目 2:10:8 比例放大 - 无敌通过 **GE + `State.Invincible`** 实现,进出窗口时施加/移除 - 位移**完全由 root motion 驱动**(见 5.7) - 只允许取消攻击**后摇**:`ActivationBlockedTags` 使用叶子标签 `State.Attack.PreWindup` / `State.Attack.ActiveHit`,不含父标签 `State.Attack` (GAS 的匹配是“持有者向上查询”,阻塞父标签会连带阻塞后摇) ### 5.5 弹反 - 帧结构 3 / 4~9 / 12 - 窗口内被命中 → 攻击者 60 帧硬直,自身 10 帧无敌 - **多段攻击立即终止**:攻击者被施加硬直时攻击技能被取消,剩余判定帧不再执行 - 窗口外被命中 → 走正常受击流程 ### 5.6 霸体 攻击方在 `SuperArmorFrames` 内挂 `State.HitStun.Immune`,受击技能把它列入 `ActivationBlockedTags`: - **伤害照常结算**(伤害由攻击方的 GE 施加,与受击技能无关) - **反应被压制**,攻击不被打断 重攻击配 25 帧霸体(覆盖前摇 + 判定),轻攻击无霸体。 ### 5.7 闪避的 root motion 处理 原始翻滚动画把位移烘在 **pelvis** 骨骼而非 root 上,引擎提取不到, 表现为“身体滚出去又弹回胶囊体”。处理方式(`-bakeRoll`): ``` root_i = root_i + (pelvis_i - pelvis_0) 的 XY 分量 ← 逐帧原样搬运 pelvis_i = pelvis_i - 同样的 XY 分量 ← 仅保留垂直起伏 ``` 于是 `身体世界位置 = 胶囊体(root motion) + pelvis` 与原轨迹**逐帧完全相等**: 动作不变形,胶囊体严格跟着动画走。命令执行前会自动备份资产到 `Saved/Backups/`。 ### 5.8 表现层(GameplayCue) 命中与弹反都会先执行 GameplayCue,再叠加一层**原生相机震动**作为默认反馈, 因此工程不依赖任何特效资产也能感受到打击: ``` 命中 → ASC->ExecuteGameplayCue(GameplayCue.Combat.Hit) ← 有 BP Cue 资产时在此播放 → UCombatCameraShakeModifier(衰减抖动,幅度随顿帧帧数缩放) 弹反 → ASC->ExecuteGameplayCue(GameplayCue.Combat.Parry) → 更强更长的抖动 ``` 相机震动实现为 `UCameraModifier` 子类而不是 CameraShake 资产:不依赖插件、不需要内容, 且抖动自带二次方衰减(重击抖得猛、收得快)。 **扩展方式**:在 `/Game` 下放一个蓝图 GameplayCue,Tag 填 `GameplayCue.Combat.Hit` / `GameplayCue.Combat.Parry`,即可与原生震动同时生效, 战斗代码一行都不用改。 ### 5.9 武器系统 武器不影响战斗逻辑,只做三件事:**决定攻击数据行**、**决定挂接插槽**、**决定动画组**。 ``` 按 E 拾取 / 丢弃 无动画,武器在「背后插槽」 按 T 拔刀 / 收刀 播对应蒙太奇,武器在「背后 ↔ 手上」插槽之间切换 ``` | 层 | 职责 | |---|---| | `ACombatWeapon` | 提供 `LightAttackID` / `HeavyAttackID`,负责挂到 socket、拔收刀动画与插槽切换 | | `ACombatTestCharacter` | `IsArmed()`(已拔出才算)、`ResolveWeaponAttackID()` 决定这一刀查哪一行 | | 攻击技能 | 只问角色"这一下用哪一行",**没有任何武器分支** | | 动画蓝图 | 读 `IsArmed()` → `Blend Poses by bool` 切换空手 / 双手移动组 | **挂接位置由骨架 socket 定义**(`WeaponHand_R` / `WeaponBack`),代码里不写偏移—— 和参考项目一样,socket 画在哪,武器就出现在哪。 **两套攻击数据互相独立**,各自按自己的动画长度定帧数: | 招式 | 帧数 | 时长 | 动画 | |---|---|---|---| | 徒手 L1/L2/L3 | 11/7/18 · 10/7/19 · 10/6/20 | 0.6s | Fists_A/B/C | | 徒手 重击 | 18/7/23 | 0.8s | Fists_D | | 双手 L1/L2/L3 | 22/14/36 · 23/15/38 · 24/15/49 | 1.2 / 1.27 / 1.47s | 2H_Attack_A/C/D | | 双手 重击 | 32/13/43 | 1.47s | 2H_Attack_D | 连招窗口按**总帧数的 60%** 开启,所以长短攻击的接续手感一致。 **武器背着时按攻击键打出的是空手拳**,因为 `IsArmed()` 只在拔出状态下为真。 **攻击距离与硬直也按武器区分**,全部在表里: | | 判定距离(离身体) | 受击硬直 | |---|---|---| | 徒手 | 80 cm | 42 / 48 帧 | | 双手武器 | **205 cm** | 54 / 62 / 70 帧 | 判定用"从身体连续向前扫",而不是"从某个点开始扫"——后者会在贴身时留下打不到的死区: ```cpp Start = 身体位置; End = 身体 + 前移量 + 盒半长×2; ``` **伤害**:徒手 8/8/12(重击 20),持刀 11/11/15(重击 26),保证一套连招打不死敌人, 能完整展示连招与弹反。 > **实现上踩到的一个坑**:拔刀 / 收刀蒙太奇是直接用 `Montage_Play` 播的, > 没有 GAS 技能负责收尾。如果蒙太奇不自动 blend out,播完会**停在最后一帧并一直占用 DefaultSlot**, > 把下面所有移动姿态都盖住,表现为"角色像被冻住"。 > 现在的做法是播前清空槽位、强制 `bEnableAutoBlendOut`、并在 92% 时长处显式 `Montage_Stop`。 ### 5.10 敌人 AI 行为树只有两个分支,感知决定行为,没有额外的战斗状态开关: ``` Root → Selector ├── Combat Attack (无感知目标 → 失败) └── Patrol (兜底) ``` | 组件 | 职责 | |---|---| | `ACombatAIController` | 视觉感知:看到玩家写黑板 `TargetActor`,丢失 3 秒后清除。参数在敌人蓝图里调 | | `UBTTask_CombatAttack` | 潜伏任务:靠近(直接推进)→ 进入射程出拳 → 等待动作结束 | | `UBTTask_Patrol` | 潜伏任务:走向出生点附近随机点 → 停留 1.5~2.5 秒 → 结束 | 完整的循环: ``` 巡逻 ──视觉发现──> 追逐 ──进入射程──> 攻击 ^ │ └────── 丢失目标 3 秒 ←──────────────┘ ``` #### 为什么不用导航寻路 场景里有 NavMesh,`Move To` 也能走通,但**这个工程的移动动画蓝图只响应直接移动输入**: 由 `MoveToActor` / 路径跟随驱动的 pawn 会平移而网格停在待机姿势(表现就是滑行)。 实测对比: | 移动方式 | 位移 | 动画 | |---|---|---| | 路径跟随 (`Move To`) | ✅ | ❌ 滑行 | | 直接输入 (`AddMovementInput`) | ✅ | ✅ 正常 | 因此 AI 与玩家统一走直接输入。代价是没有真正的路径规划,用两条启发式补足: 1. **前向探测避障**:向前 160cm 做线性检测,被挡则依次尝试 ±40° / ±70° / ±100° 2. **卡死恢复**:每 1.5 秒检查位移,不足 25cm 就放弃当前巡逻点重新选 > 若把动画蓝图的速度源改为 `CharacterMovement->Velocity`,可以切回标准导航寻路, > 那时代码只需把两个任务里的移动调用替换为 `Move To` 节点。 **受击硬直可以刷新**:受击技能开启了 `bRetriggerInstancedAbility`,连招中的第二、 第三下会用**本次攻击自己的硬直时长**重启受击,而不是被第一次的硬直挡住。 之前这个技能把 `State.HitReact` 加进了自己的 `ActivationBlockedTags`, 导致后续命中静默失效、敌人按第一次的短硬直计时起身。 **被控时不能移动**:行为树每帧都会请求移动,所以移动入口(`MoveAlongPathToLocation` / `MoveAlongPathToActor` / `Think`)都先检查 `IsMovementIncapacitated()` (`HitReact` / `Stagger` / `Attack` / `Dead`),命中则立即 `StopMovementImmediately`。 否则敌人会一边"硬直"一边朝玩家滑。 ### 5.11 死亡 优先级最高。触发时依次:取消所有战斗技能 → 清除全部战斗标签 → 关闭受击碰撞 → 锁定输入 → 清空输入缓冲 → 播放死亡动画 → 动画结束帧触发布娃娃。 --- ## 6. 状态标签(全部为 GameplayTag,无布尔量) | 分类 | 标签 | |---|---| | 攻击 | `State.Attack` / `.PreWindup` / `.ActiveHit` / `.Recovery` / `.ComboWindow` | | 闪避 | `State.Dodging` / `.Startup` / `.Invincible` / `.Recovery` | | 防御 | `State.Parrying` / `.Startup` / `.Recovery`、`State.ParryWindow` | | 受击 | `State.HitReact`、`State.HitStun`、`State.HitStun.Immune`、`State.Stagger` | | 通用 | `State.Invincible`、`State.Frozen`、`State.Dead`、`State.Idle` | 所有互斥/取消/白名单判断都基于标签容器,新增状态不需要改任何 if 分支。 --- ## 7. 帧时序图(GA ↔ 动画) ```mermaid sequenceDiagram participant Clock as FrameClock(60Hz) participant Actor as ActorFrameComponent participant GA as MeleeAttack participant Anim as AnimInstance participant V as 受击方 Clock->>Actor: OnFrame(GlobalFrame) Actor->>GA: OnCombatFrame(1) GA->>GA: ApplyTag(State.Attack.PreWindup) GA->>Anim: Montage_Play(PlayRate = 切片/帧预算) Actor->>GA: OnCombatFrame(11) GA->>GA: ApplyTag(State.Attack.ActiveHit) GA->>V: BoxTrace 命中 → 伤害 GE GA->>Actor: RequestFreeze(HitStop) V->>V: RequestFreeze(HitStop) Note over Actor,V: 冻结期间双方帧游标暂停 Actor-->>GA: OnExpired GA->>V: 击退 + Event.Combat.HitReact Actor->>GA: OnCombatFrame(22..36) 连招窗口 GA->>GA: 缓冲有输入 → StartAttack(下一段) Actor->>GA: OnCombatFrame(37) GA->>GA: RemoveTag / EndAbility ``` 轻攻击 1 的帧划分: ``` 帧 1 ───── 11 ───── 18 ───────────── 22 ───── 36 │ 前摇 11 │ 判定 7 │ 后摇 19 │连招窗口│ │ │命中检测 │ │ │ ``` --- ## 8. 网络扩展性 当前为**单机**实现,但结构按可联机的方式设计: | 设计点 | 联机收益 | |---|---| | 逻辑帧与渲染帧解耦 | 可用固定帧号做状态同步/回滚,帧号即逻辑时间 | | 状态全部是 GameplayTag | 天然可复制(GameplayTagContainer 是 GAS 原生复制字段) | | 时序全部在 DataTable | 客户端/服务端读同一份数据,不会数值不一致 | | 技能基于 GAS | 可切换 NetExecutionPolicy 为 ServerInitiated,预测与回滚由 GAS 提供 | | 伤害/无敌为 GameplayEffect | GE 自带复制与预测支持 | | 位移来自 root motion | 服务端权威位移 + 客户端预测,不易校验失败 | **待补齐的联机工作**: 1. 攻击技能改为 `ServerInitiated`,客户端只做预测表现 2. 命中判定在服务端执行,客户端用 GameplayCue 播表现 3. 顿帧改为客户端本地表现,不影响逻辑帧推进 4. 输入缓冲需携带预测帧号,服务端重放时按帧号对齐 --- ## 9. 验收自测清单 | # | 验收项 | 结果 | 验证方式 | |---|---|---|---| | 1 | 逻辑帧率严格 60 FPS | ✅ | `combat.DebugFrameClock 1` 打印帧间隔 | | 2 | 轻攻击三段连招 + 轻重派生 | ✅ | 连按 F;F→R 派生重击 | | 3 | 缓冲深度 2、15 帧有效、溢出丢弃 | ✅ | 连按 5 次,日志出现 Buffer full | | 4 | 窗口外不可接续 | ✅ | 窗口外按键只起新招,日志无 Chain | | 5 | 命中双方顿帧 N 帧 | ✅ | 日志 hit stop N frames,动画暂停 | | 6 | 击退 → 硬直(顺序正确) | ✅ | 冻结结束后才位移,随后播受击 | | 7 | 闪避无敌窗口内不受伤 | ✅ | 窗口内被攻击不掉血 | | 8 | 闪避可取消后摇,不可取消前摇/判定 | ✅ | 后摇中按 G 生效,前摇中无效 | | 9 | 弹反成功 10 帧无敌 + 60 帧硬直 | ✅ | 日志 was PARRED | | 10 | 多段攻击被弹反后立即终止 | ✅ | 弹反后剩余判定帧不产生伤害 | | 11 | 死亡最高优先级,清状态/输入/碰撞 | ✅ | 死亡后按键无响应 | | 12 | 三段连招使用不同动画 | ✅ | Fists_Attack_A / B / C | | 13 | 重攻击霸体 | ✅ | 霸体期间掉血但不被打断 | | 15 | 命中/弹反有表现反馈 | ✅ | 命中处粒子 + 屏幕抖动,弹反粒子与震动更强 | | 16 | 敌人巡逻 → 视觉发现 → 追击 → 攻击 → 丢失 3 秒回到巡逻 | ✅ | 日志 Sighted / Lost sight / Forgot the target | | 17 | 输入缓冲溢出丢弃 | ✅ | 连按 5 次,日志出现 Buffer full | | 20 | 武器拾取 / 拔刀 / 收刀 / 攻击集切换 | ✅ | E 拾取(背后)→ T 拔刀(手上)→ F 挥剑 → T 收刀 | | 18 | 闪避不能取消攻击前摇/判定 | ✅ | 日志 Dodge rejected while in [State.Attack.PreWindup] | | 19 | 攻击期间原地转向 | ✅ | 日志 turned N deg | | 14 | 攻击期间禁止平移、原地转向 720°/s | ✅ | 出招时按 W 不位移,转视角角色跟随 | --- ## 10. 已知限制 1. **动画为占位资源**:三段轻攻击使用 FCS 资源包的空手动作,重攻击复用第四段。 2. **AI 无路径规划**:用前向探测 + 卡死重选代替寻路,U 形死角可能绕不出(但不会永久卡死)。 3. **弹反动画**使用格挡循环前 0.45 秒切片,并非专门的弹反动作。 4. **无音效**:打击与弹反有粒子特效(`P_FistsImpact` / `P_BlockImpact`)和相机震动, 但没有声音;放入蓝图 GameplayCue 后会自动叠加。 5. **持械时脚部 IK 被削弱**:模板动画蓝图里的 Control Rig(Foot IK)按空手站姿的骨盆高度 计算脚部目标,持械的大跨步姿势下补偿量被放大、腿会被拉变形。现在用 `Select(Armed, 0.1, 1.0)` 控制它的 Alpha:空手保持完整 IK,持械只保留 10% 的微调。 彻底修好需要按姿势分别调 IK 参数,属于画面精修,未做。 5. **布娃娃**使用引擎默认物理资产,未做专项调校。 ## 11. 生成与诊断命令 ```powershell # 重新生成四张数据表(会覆盖手工改动) UnrealEditor-Cmd.exe CombatTest.uproject -run=CombatGenerateData # 打印所有攻击行 ... -run=CombatGenerateData -dump # 打印动画源/重定向长度对比 ... -run=CombatGenerateData -animreport # 将 pelvis 位移烘到 root(执行前自动备份) ... -run=CombatGenerateData -bakeRoll # 打印烘焙后的骨骼轨迹 ... -run=CombatGenerateData -dumpRoll ```