docs(ai): 敌人 AI 框架(BrainGraph)设计——HFSM 替换 Behavior Designer
C# 声明式为唯一事实来源, 可视化从模型自动生成; 只新建决策层+干净能力接口, 现有感知/移动/能力子系统保留; 受击物理FSM用 IsControllable 门显式让位; 交付面: 运行时调试器/Mermaid导出/MCP自省API; v1 试点 E001+ChaoFengBoss. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
# 敌人 AI 框架设计(BrainGraph)
|
||||
|
||||
- 日期:2026-07-03
|
||||
- 状态:设计已确认,待 review → 转实现计划
|
||||
- 工作名:**BrainGraph**(最终名待定)
|
||||
- 命名空间:`BaseGames.AI`(独立 asmdef,零三方依赖)
|
||||
- 目标:以一套自研、C# 声明式、可 MCP 全自动生成/调试的**分层状态机(HFSM)**决策框架,替换 Behavior Designer(Opsive)作为敌人/NPC AI 的"决策大脑"。
|
||||
|
||||
> 规范约束:本设计与最终代码的命名、注释、`[Tooltip]`/`[Header]` 中**不得出现任何参考游戏名**(项目 CLAUDE.md 第 4 条)。讨论中引用的商业游戏仅作内部设计参照,不落代码。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
### 1.1 现状(探查结论)
|
||||
|
||||
项目采用"自研外壳 + Opsive Behavior Designer 决策层"双层设计,但决策层处于**"节点库就绪、行为树资产为空"**的半成品状态:
|
||||
|
||||
- Opsive Behavior Designer v2(GraphDesigner 内核)作为 UPM 包安装,49 个自定义 `BD_*` 任务节点已写好并编译(`GRAPH_DESIGNER` 宏开启)。
|
||||
- **但没有任何行为树图资产(graph .asset),也没有任何敌人 prefab/场景挂载 `BehaviorTree` 组件。** 决策层是空的。
|
||||
- `EnemyBase` 已写好 BT 的 Manual-Tick + 5 档 LOD 节流集成(`EnemyBase.cs:606/629`),但因无图资产而未接通。
|
||||
- 感知/状态/执行三层已完整实现且解耦良好;49 个 `BD_*` 任务全部是**薄壳**,仅调用 `EnemyBase` 门面方法,不直接碰子系统。
|
||||
|
||||
### 1.2 痛点
|
||||
|
||||
Behavior Designer 的行为树是**手绘的二进制图资产**:
|
||||
|
||||
- MCP / 代码**无法生成**,无法 diff,无法自动化调试。
|
||||
- 决策逻辑的"唯一事实来源"是二进制图,git review 不可读。
|
||||
|
||||
因替换沉没成本几乎为零(无既有图资产),现在是重构决策层的最佳时机。
|
||||
|
||||
### 1.3 核心设计取向(已与用户逐条确认)
|
||||
|
||||
1. **范式**:分层状态机(HFSM)为骨干;帧级攻击编排下沉到现有能力系统,决策层只负责"选招/编排/让位"。**不采用行为树**——对编排式、确定性、帧级精确的横版动作敌人,FSM 更可控、更好调试。
|
||||
2. **唯一事实来源**:C# 声明式(fluent builder)→ 构建成可自省的不可变 `AiGraph` 模型 →**既执行、又生成可视化**。参数(数值/冷却/距离/权重)外挂 SO 热调。
|
||||
3. **边界**:只新建"决策大脑 + 干净能力接口(`ISensor`/`IMover`/`ICombatant`/`IActorVitals`)";现有感知/移动/能力子系统保留实现,用薄适配器包接口,彻底甩开 `EnemyBase` 上帝门面。
|
||||
4. **交付面**:运行时实时调试器 + Mermaid/dot 静态导出 + MCP 运行时自省 API。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构总览
|
||||
|
||||
```
|
||||
┌─ 声明层 (C# 唯一事实来源) ───────────────────────────┐
|
||||
│ EnemyAiScript.Build(b) 用 fluent 描述状态/转换/编排 │ ← MCP 直接写/diff
|
||||
└───────────────┬─────────────────────────────────────┘
|
||||
│ 构建一次(每种敌人一份,实例间共享 = 省内存/零结构GC)
|
||||
┌─ 模型层 (不可变 AiGraph) ────────────────────────────┐
|
||||
│ States · Transitions · AbilitySequences —— 可自省 │ ← 执行 + 可视化 同源
|
||||
└───────────────┬───────────────────────┬─────────────┘
|
||||
执行 │ │ 只读投影
|
||||
┌─ 运行时 (每实例 AiRuntime) ──────┐ ┌─ 交付面 ───────────────┐
|
||||
│ 当前状态指针·黑板·计时器·序列游标 │ │ Mermaid导出 / 实时调试器 │
|
||||
│ 事件驱动转换 + LOD 节流 Tick │ │ / MCP自省API │
|
||||
│ IsControllable 门(受击让位) │ └─────────────────────────┘
|
||||
└───────────────┬─────────────────┘
|
||||
│ 只依赖接口
|
||||
┌─ 能力接口层 (甩开 EnemyBase 门面) ───────────────────┐
|
||||
│ ISensor · IMover · ICombatant · IActorVitals │
|
||||
└───────────────┬─────────────────────────────────────┘
|
||||
│ 薄适配器
|
||||
┌─ 执行层 (保留现有实现) ──────────────────────────────┐
|
||||
│ PhysicsPerceptionSystem · EnemyMovement/NavAgent · │
|
||||
│ EnemyAbilityRegistry(+子类) · EnemyStats · Poise … │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心运行时模型
|
||||
|
||||
### 3.1 `AiGraph`(不可变模型,每种敌人一份,实例共享)
|
||||
|
||||
- 声明一次即构建,包含所有 `State`、`Transition`、`AbilitySequence`。
|
||||
- 被运行时执行,也被导出器/调试器读取 —— **同一模型,图与代码绝不失同步**。
|
||||
- flyweight 共享 → 结构零 per-instance 分配、缓存友好;委托在构建期捕获一次。
|
||||
|
||||
### 3.2 `AiRuntime`(每敌人实例,极轻)
|
||||
|
||||
持有:当前状态路径、`Blackboard`、计时器、正在跑的 `AbilitySequence` 游标。热路径无 LINQ / 装箱。
|
||||
|
||||
### 3.3 状态 · 层级 · 转换
|
||||
|
||||
- **State** = 可选 `OnEnter/Tick/OnExit` + 出边转换 + 可选子状态机(层级)+ 可选能力编排 `Do(...)`。
|
||||
- **层级(HFSM)**:父状态转换始终高优先评估。"任意态→受击/死亡/被弹反"挂在父层,一处声明、全局生效,替代 BD 的 Selector 优先级堆叠。
|
||||
- **两类转换,混合驱动**:
|
||||
- **事件转换(push)**:受击、被弹反、发现玩家等信号即时触发,不靠轮询。慢速巡逻杂兵被打的瞬间也立即反应。
|
||||
- **条件转换(pull)**:谓词(`InAttackRange` / `LostFor(2s)`),仅在该状态 LOD 频率下评估。
|
||||
- 按优先级有序、首个命中即转 → 确定性、好调试。
|
||||
|
||||
### 3.4 能力编排(`AbilitySequence`,非帧级微时序)
|
||||
|
||||
**关键分工**:帧级编排(一招怎么打,HitBox 时间窗)留在现有 `EnemyAbility` 子类里,本框架**不重造**。决策层的"序列"= 能力编排:
|
||||
|
||||
```
|
||||
选招(按 SO 的 preferredRange/priority/exclusion) → Execute()
|
||||
→ while(IsAbilityRunning) 等待(可被 InterruptAll 打断)
|
||||
→ AbilityEnded → 重评估 / 接下一招
|
||||
```
|
||||
|
||||
真要新增全新攻击 → 写一个 `EnemyAbility` 子类(属能力系统职责,不归 AI 框架)。
|
||||
|
||||
### 3.5 上下文 = 类型化能力接口 + 小黑板(混合)
|
||||
|
||||
- **类型化能力接口**(见 §4):状态逻辑调用,无装箱、编译期安全、摆脱 `EnemyBase` 门面。
|
||||
- **小黑板**(具名可枚举):只放 AI 临时值(目标、最后已知位置、计时器、弹反标志、hp%…)。保留具名黑板是为了让**调试器/MCP 能通用枚举"当前黑板值"**。
|
||||
|
||||
### 3.6 受击物理 FSM 的共存(`IsControllable` 显式让位)
|
||||
|
||||
`AiPhase`(决策层)与 `EnemyStateType{Controlled,Hurt,Stagger,KnockUp,Dead}`(受击物理 FSM)正交并存。接管规则:
|
||||
|
||||
- `IActorVitals.IsControllable = (CurrentState == Controlled)`。
|
||||
- **BrainGraph 只在 `IsControllable` 为真时推进决策**;被打成 Hurt/Stagger/KnockUp 时决策层**挂起**(保留状态指针、不下发新指令;正在等待的能力早被 `TakeDamage` 内 `InterruptAll(reason)` 打断)。物理态回 `Controlled` → 原地重评估。
|
||||
- 弹反/受击/死亡额外作为**父层事件转换**(`OnParried→反应态`、`OnDied→终止`),高优先即时。
|
||||
- 好处:调试器能显式显示"决策层:已挂起(KnockUp 中)",不靠隐式 fail 猜测。
|
||||
|
||||
### 3.7 Tick 与性能
|
||||
|
||||
- **LOD 调度**:idle/离屏慢 tick、战斗每帧;中央 `AiScheduler` 时间片分摊避免帧尖峰;**事件转换不受 LOD 影响、永远即时**。
|
||||
- 共享不可变定义 + 极小实例态 → 低 GC、cache 友好。
|
||||
|
||||
---
|
||||
|
||||
## 4. 能力接口 ↔ 现有实现 1:1 映射
|
||||
|
||||
所有接口方法均有真实实现背书(file:line 见探查报告)。距离比较统一在 `ISensor` 内部用平方距离(对齐 `EnemyStats.SqrDistanceToPlayer`)。
|
||||
|
||||
| 接口 | 方法(节选) | 背后真实实现 |
|
||||
|---|---|---|
|
||||
| **ISensor** | `SeesPlayer()` / `HasLineOfSight` / `InRange(r)` / `SlotDetects(slot)` / `LostFor(t)` / `LastKnown` | `EnemyBase.IsPlayerVisible()`(ThreatAssessor) · `HasAnyDetection(LOS\|Sight)` · `IsPlayerInRange`(平方) · `IPerceptionSystem.HasAnyDetection(slot)` · 黑板计时 · 黑板(原 `LastKnownPlayerPosition`) |
|
||||
| **IMover** | `MoveTo(p)` / `MoveDir(d)` / `Stop()` / `Face*/FacePlayer()` / `JumpTo(p)` / `LookAround()` / `WalkRandom()` / `AtDestination` / `IsGrounded/IsNearEdge` / `ReturnHome()` | `IPathAgent.RequestMoveTo` · `EnemyMovement.MoveHorizontal` · `StopMovement` · `FaceTarget/FaceDirection` · `JumpToTarget` · `BeginLookAround` · `WalkToRandom` · `IsAtDestination` · `IsGrounded/IsNearEdge` · `HomePosition` |
|
||||
| **ICombatant** | `UseAbility(id)` / `ForceUseAbility(id)` / `CanUseAbility(id)` / `IsAbilityRunning(id?)` / `AbilityPhase(id)` / `InterruptAbilities(reason)` / `PickUsable()` / `BasicAttack(type)` / `CanAttack()` | `Registry.Get(id).Execute()/ForceExecute()` · `ability.CanUse/IsRunning/Phase` · `Registry.InterruptAll/InterruptGroup` · 按 `EnemyAbilitySO.preferred*Range/priority/exclusionGroup` 选招 · `BeginAttack(AttackType)` |
|
||||
| **ICombatant.Boss** | `UseSkill(id)` / `UseSkillWeighted()` / `IsSkillExecuting` / `CurrentPhase` / `BeginPhaseTransition(n)` / `IsPhaseTransitioning` / `SetWeakPoints(on,mult)` | `BossBase.UseBossSkill` · `UseBossSkillWeighted` · `IsBossSkillExecuting` · `CurrentPhase` · `BeginPhaseTransition` · `WeakPointSystem.SetActive` |
|
||||
| **IActorVitals** | `HpPercent` / `HpBelow(r)` / `IsAlive` / `IsInvincible` / `IsControllable` / `PhysicalState` / `HasStatusEffect(t)` / `ApplyStatusEffect` / `PoiseLevel`/`SetPoiseLevel` | `EnemyStats.CurrentHP/MaxHP` · `IsHPBelow` · `IsAlive/IsInvincible` · `CurrentState==Controlled` · `EnemyStatusEffectManager` · `EnemyPoiseComponent` |
|
||||
|
||||
### 4.1 推送信号总线(事件转换来源)
|
||||
|
||||
`AiSignal { Damaged, Parried, Staggered, KnockedUp, Died, PlayerSpotted, AbilityEnded, PhaseChanged }`
|
||||
|
||||
来源接线(多数已有 C# event):`TakeDamage`(→Damaged) · `ReceiveParry`(→Parried,取代轮询 `ConsumeParryEvent`) · `ForceState`(→Staggered/KnockedUp) · `PerformDeath`(→Died) · `ThreatAssessor` 由假变真(→PlayerSpotted) · `ability.Interrupted`/协程结束(→AbilityEnded) · `BossPhaseEvent`(→PhaseChanged)。
|
||||
|
||||
### 4.2 参数外挂(热调落到真实字段)
|
||||
|
||||
声明里的 `c.P.*` 解析到已有 SO/字段:`EnemyStatsSO`(`AttackRange/AttackCooldown/DetectRange/MaxChaseDistance/LoseLinkTimeout/AlertDuration/InvestigateDuration/HomeRadius/HitTiers…`)、各 `EnemyAbility` 子类序列化字段、`EnemyAbilitySO`(`cooldown/preferredMin/MaxRange/requiresLineOfSight/priority/exclusionGroup`)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 声明层示例(唯一事实来源)
|
||||
|
||||
```csharp
|
||||
[AiDefinition("E001")]
|
||||
public sealed class BasicMeleeGruntAi : EnemyAiScript
|
||||
{
|
||||
protected override void Build(BrainBuilder b)
|
||||
{
|
||||
b.Entry("Patrol");
|
||||
|
||||
// 父层:全局反应(事件转换,高优先即时)
|
||||
// 注意:普通受击/硬直/击飞的"让位"由 IsControllable 门自动处理,无需在此声明。
|
||||
// 父层事件转换只用于决策层需要的、超出物理动画的额外行为响应。
|
||||
b.Global()
|
||||
.To("Dead").OnEvent(AiSignal.Died) // 终止决策
|
||||
.To("Enrage").When(c => c.Vitals.HpBelow(c.P.EnrageHpRatio)); // 低血狂暴(示例)
|
||||
|
||||
b.State("Patrol")
|
||||
.Tick(c => c.Mover.WalkRandom())
|
||||
.To("Chase").When(c => c.Sensor.SeesPlayer());
|
||||
|
||||
b.State("Chase")
|
||||
.Tick(c => c.Mover.MoveTo(c.Blackboard.LastKnown))
|
||||
.To("Combat").When(c => c.Sensor.InRange(c.P.MeleeRange))
|
||||
.To("Search").When(c => c.Sensor.LostFor(c.P.LoseLinkTimeout));
|
||||
|
||||
b.State("Combat")
|
||||
.OnEnter(c => c.Mover.FacePlayer())
|
||||
.Do(Seq.Select( // 按 SO 的 preferredRange/priority/exclusion 选招并等待其结束
|
||||
Ability("slash").When(c => c.Sensor.InRange(c.P.MeleeRange))))
|
||||
.To("Chase").When(c => !c.Sensor.InRange(c.P.MeleeRange) && c.Combat.NoAbilityRunning);
|
||||
|
||||
b.State("Search")
|
||||
.OnEnter(c => c.Mover.LookAround())
|
||||
.To("Chase").When(c => c.Sensor.SeesPlayer())
|
||||
.To("Patrol").After(c => c.P.InvestigateDuration);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 转换条件的可读标签由 `[CallerArgumentExpression]` 自动从 lambda 源码文本抓取(可选覆写)。
|
||||
- `c.P.*` = 参数视图,解析自 `EnemyStatsSO` / 能力 SO。
|
||||
|
||||
---
|
||||
|
||||
## 6. 交付面
|
||||
|
||||
### 6.1 Mermaid/dot 导出器
|
||||
|
||||
- 纯函数 `AiGraph → stateDiagram-v2` 文本;层级子机 → composite state;边标签取自 `[CallerArgumentExpression]` 抓到的条件文本 / 事件枚举名。
|
||||
- 入口:菜单 `BaseGames/AI/Export Graph` + 静态方法(打印 Console,供 MCP `unity_execute_code` 抓);文件写 `Docs/AI/<enemyId>.md`。
|
||||
|
||||
### 6.2 运行时实时调试器(Editor 窗口,仅编辑器)
|
||||
|
||||
- 选中敌人 → 实时高亮当前状态、最近 N 次转换(from→to + 触发原因)、黑板表、正在跑能力及 `Phase`/进度、`IsControllable` 门状态。
|
||||
- 全局 `AiScheduler.Paused` + 单步。
|
||||
- MVP:状态列表 + 实时高亮 + trace + 黑板;完整自动布局节点图归入后续"设计期只读窗口"。
|
||||
|
||||
### 6.3 MCP 运行时自省 API
|
||||
|
||||
- 运行时服务 `AiDebugService`(注册进 ServiceLocator),静态入口暴露:
|
||||
- `ListAgents()` / `GetAgent(id)` → `{ currentStatePath, isControllable, lastTransitions[], blackboard{}, runningAbility, physicalState }`
|
||||
- `ExportMermaid(enemyTypeId)`、`SetPaused(bool)`、`Step()`、`ForceState(id, stateName)`(调试驱动)
|
||||
- MCP 经 `unity_execute_code` 调用即可"问诊"任意敌人 → 生成 + 调试闭环。
|
||||
|
||||
---
|
||||
|
||||
## 7. 组件、装配与脚手架
|
||||
|
||||
### 7.1 组件
|
||||
|
||||
- 新增 `EnemyAiBrain : MonoBehaviour`:按 id 从注册表取共享 `AiGraph`、建 `AiRuntime`、`Awake` 收集兄弟子系统构造四适配器、按 LOD tick。**取代** `EnemyBase` 中 `#if GRAPH_DESIGNER` 的 BT 集成。
|
||||
- `EnemyAiScript`(抽象基类)+ `[AiDefinition("<id>")]` 特性;反射注册表 `AiDefinitionRegistry`。
|
||||
- 适配器 `SensorAdapter/MoverAdapter/CombatantAdapter/VitalsAdapter`:直接引用具体子系统(`Movement/Nav/Registry/Perception/Stats/StatusEffects/Poise`),不经门面。
|
||||
|
||||
### 7.2 Domain Reload 约束
|
||||
|
||||
项目已关 Domain/Scene Reload。`AiDefinitionRegistry`(静态注册表)与任何静态缓存须用 `RuntimeInitializeOnLoadMethod(BeforeSceneLoad)` 重置;`AiRuntime` 运行时态随 `EnemyAiBrain` 生命周期,无跨播放态残留。
|
||||
|
||||
### 7.3 脚手架接线(遵项目 CLAUDE.md 第 2 条)
|
||||
|
||||
扩展 `SceneObjectPlacerTool` / `CharacterWizardWindow`:放置敌人时自动挂 `EnemyAiBrain` + 绑定 `AiDefinition` id + 参数 SO。消除 `SceneObjectPlacerTool.cs:354/524` 的 `★ 手动挂行为树` TODO,使敌人创建全链路 MCP 可自动化。
|
||||
|
||||
---
|
||||
|
||||
## 8. 迁移路径(先共存、后退役)
|
||||
|
||||
1. 建框架 + 适配器,先在杂兵 `E001_CaoZhi` 跑通(BD 仍在、该敌人不用)。
|
||||
2. 上齐三交付面(导出 / 调试器 / MCP 自省)。
|
||||
3. 拿 Boss `ChaoFengBoss` 验证阶段 / 加权技能 / 弱点窗口。
|
||||
4. 扩展脚手架自动接线。
|
||||
5. 代表性敌人 parity 后 → 批量迁移剩余 → **最后**删除 49 个 `BD_*`、`EnemyBase` BT 集成、`com.opsive.*` 包。
|
||||
|
||||
> 试点目标(`E001_CaoZhi` / `ChaoFengBoss`)为默认假设,可在 review 时调整。
|
||||
|
||||
---
|
||||
|
||||
## 9. v1 范围
|
||||
|
||||
**纳入 v1**:
|
||||
- 核心运行时(`AiGraph` 模型 + `BrainBuilder` + `AiRuntime` + 层级 + 事件/条件转换 + 能力编排 `AbilitySequence` + `IsControllable` 门 + `AiScheduler` LOD 调度)。
|
||||
- 4 能力接口 + 适配器。
|
||||
- `AiSignal` 信号总线接线(对 `EnemyBase` 的最小改动:受击/弹反/死亡/发现玩家/能力结束/阶段变更处 raise 信号)。
|
||||
- `EnemyAiBrain` 组件 + `AiDefinitionRegistry`。
|
||||
- 三交付面(调试器取 MVP)。
|
||||
- 脚手架接线。
|
||||
- 试点:1 杂兵(`E001_CaoZhi`)+ 1 Boss(`ChaoFengBoss`)。
|
||||
|
||||
**推迟到后续**:
|
||||
- 完整节点图 GraphView 设计期只读窗口。
|
||||
- 数据驱动"新招式免写 C# 子类"(招式继续走 `EnemyAbility` 子类)。
|
||||
- 批量迁移剩余敌人 + 彻底删 BD/Opsive(parity 后单独收尾)。
|
||||
- 群体协同精修(先薄封装现有 `AlertNearby`/`ReceiveAlert`)。
|
||||
|
||||
---
|
||||
|
||||
## 10. 主要程序集与文件(预期)
|
||||
|
||||
- 新 asmdef:`BaseGames.AI`(`Assets/_Game/Scripts/AI/`),引用 `BaseGames.Enemies` / `BaseGames.Core`;不引用 `Opsive.*`。
|
||||
- 运行时:`AiGraph.cs`、`State.cs`、`Transition.cs`、`AbilitySequence.cs`、`AiRuntime.cs`、`Blackboard.cs`、`BrainBuilder.cs`、`EnemyAiScript.cs`、`AiDefinitionRegistry.cs`、`AiScheduler.cs`、`EnemyAiBrain.cs`。
|
||||
- 接口 + 适配器:`ISensor/IMover/ICombatant/IActorVitals` + 4 个 `*Adapter.cs`。
|
||||
- 交付面:`MermaidExporter.cs`、`AiDebugService.cs`、`Editor/BrainGraphDebuggerWindow.cs`。
|
||||
- 改动:`EnemyBase.cs`(移除 BT 集成、raise `AiSignal`)、`SceneObjectPlacerTool.cs` / `CharacterWizardWindow.cs`(自动接线)。
|
||||
|
||||
---
|
||||
|
||||
## 11. 验收标准(v1)
|
||||
|
||||
1. `E001_CaoZhi` 完全由 BrainGraph 驱动,行为达到"巡逻→发现→追击→近战选招→丢失搜查→归位"闭环,且**不挂任何 `BehaviorTree` 组件**。
|
||||
2. `ChaoFengBoss` 由 BrainGraph 驱动多阶段 + 加权技能 + 弱点窗口,`BeginPhaseTransition` 无敌演出正确衔接。
|
||||
3. 受击/弹反:被打时决策层经 `IsControllable` 门正确挂起、能力被打断、恢复后重评估;弹反经父层事件转换进入 Stagger 反应。
|
||||
4. `BaseGames/AI/Export Graph` 能为上述两个敌人导出与运行行为一致的 Mermaid 图。
|
||||
5. 运行时经 MCP `unity_execute_code` 调 `AiDebugService.GetAgent(id)` 能取到当前状态/转换原因/黑板/能力/门状态。
|
||||
6. 脚手架放置敌人时自动挂 `EnemyAiBrain` 并绑定定义 + 参数 SO(无手动步骤)。
|
||||
7. 通过项目自检:SO 校验、AddressKey 校验、Physics2D 层校验无新增错误。
|
||||
Reference in New Issue
Block a user