前言
前两篇整理的是玩家状态和跳跃手感。这部分继续看战斗相关内容:敌人怎样切换状态、属性怎样计算、Buff 放在哪里,以及技能冷却和旧技能树目前是什么情况。
MetroidvaniaLike 目前用 Entity 提供共同能力,Player 和 Enemy 再各自处理输入或 AI。重新检查代码时也发现,旧技能树已经标记为废弃,这里会按现状写清楚。
项目源码:qian488/MetroidvaniaLike。
这个系列
- 用类状态机组织动作角色
- 土狼时间、输入缓冲与动作打断
- 本文:敌人 AI、属性与技能系统
Entity 里放了哪些共同能力
玩家由输入驱动,敌人由感知、距离、冷却和战术条件驱动。二者的切换原因不同,但底层都有:
- 当前状态与 Enter/Update/Exit;
- 移动、朝向和物理检测;
- 血量、受击、击退和死亡;
- 动画触发器;
- 属性与状态效果。
因此项目用 Entity 提供共同实体能力,Player 与 Enemy 在其上组合各自控制逻辑:
Entity
├── EntityHealth
├── EntityCombat
├── EntityStats
├── EntityStatusHandler
└── EntityVFX
Player:输入驱动状态
Enemy:感知和距离驱动状态
Player 和 Enemy 共享移动、受击、属性等实体能力。Player 的决策输入来自玩家操作,Enemy 的决策输入来自感知、距离和冷却。
敌人的状态切换
典型敌人包含 Idle、Move、Battle、Attack、Stunned 和 Dead:
Idle
└─发现玩家→ Battle
Battle
├─距离过远→ Move
├─进入攻击范围且冷却完成→ Attack
└─失去目标→ Idle
Attack
└─动画完成→ Battle
任意可受控状态
├─受到强控制→ Stunned
└─生命归零→ Dead
Battle 作为警觉/决策状态很有用:它避免 Idle、Move 和 Attack 两两直接耦合,也给转身、后撤、等待冷却等行为留下空间。
随着兵种增加,可以把目标、距离、视线、上次受击时间和技能冷却整理成感知上下文。Battle 状态读取上下文并选择移动或攻击,感知计算保持独立。
近战和远程敌人怎样复用
近战与远程敌人的差异主要在攻击执行与安全距离:
- 移动、受伤、死亡等状态可以复用;
- AttackState 可以由子类实现近战判定或生成投射物;
- BattleState 根据理想距离选择靠近、保持或后撤;
- 伤害仍通过统一
EntityCombat结算。
如果为了弓箭手复制完整敌人状态机,后续修复死亡或眩晕 Bug 时就要维护多份逻辑。更合适的是复用状态骨架,通过组合攻击策略表达差异。
属性和 modifier
最初直接写 attack、defense 和 moveSpeed 很方便,但装备、技能和 Buff 加入后,同一属性会有多种来源:
基础值
+ 固定加成
+ 装备加成
× 百分比修正
× 临时状态倍率
= 最终值
项目用 Stat 包装基础值和 modifier,再由 EntityStats 按资源、攻击、防御等组管理。ScriptableObject 提供实体的默认数值配置,多个 prefab 可以复用同一份设置。
每次修改都保留来源。卸下装备时移除对应 modifier,最终属性随后重新计算。
属性计算还需要定义明确顺序。加法、乘法、取整和上下限顺序不同会产生不同结果,应由一处公式决定并通过测试固定。
Buff 和 Debuff 放在哪里
减速、中毒、眩晕等效果有持续时间、来源和叠加规则。如果 Buff 开始时直接把 moveSpeed *= 0.5,结束时再除回去,多来源叠加和提前移除很容易出错。
更可靠的模型是:
EntityStats:保存裸属性和长期 modifier
EntityStatusHandler:保存当前 Buff/Debuff 实例
EntityCombat:查询最终属性并执行伤害
状态实例至少需要:
- 类型与唯一标识;
- 来源;
- 持续时间或剩余 tick;
- 数值与加成方式;
- 叠加、刷新或互斥规则;
- 添加和移除时的副作用。
眩晕会请求行为状态切换,减速添加属性 modifier,持续伤害定时调用战斗结算。EntityStatusHandler 通过这些明确入口协调效果,避免 Buff 直接查找场景组件。
技能数据、冷却和具体效果
项目的 SkillBase 负责技能类型、升级类型、冷却和最近使用时间;ScriptableObject 承载图标、描述和配置;具体技能组件实现冲刺、时间回声或治疗等行为。
可以把技能拆成三层:
SkillData
名称、图标、冷却、数值、升级配置
SkillRuntime
是否解锁、当前等级、剩余冷却、使用条件
SkillBehaviour
位移、伤害、生成对象、施加状态等实际效果
UI 读取技能数据和运行时状态,点击后向技能服务提出使用请求。位移和伤害由技能行为与战斗模块执行。
如果 lastUsedTime 初始为 0,而游戏开始时 Time.time < cooldown,技能会被误判为仍在冷却。初始化阶段需要显式设置技能为可用状态。
旧技能树已经废弃
公开仓库 README 展示了技能树能力,但项目笔记也记录了旧 UI_SkillTree 已进入废弃状态:它与后续 UI 框架方向不一致,数据、解锁状态和表现存在耦合。
这个旧实现目前可以确认以下状态:
- 已验证节点、连线和解锁交互;
- 当前实现已经废弃,不再作为长期维护版本;
- 后续应让技能数据进入配置、解锁状态进入存档、UI 只负责显示;
- 旧代码应移入 Legacy 或从构建中剥离,避免误用。
指出技术债比把所有模块描述成“完善”更有价值,因为它说明开发者能识别边界和演进方向。
一次攻击经过哪些模块
一次攻击可以组织成:
AttackState 在动画命中帧提出攻击
↓
Hitbox 查询目标
↓
EntityCombat 根据双方属性计算伤害
↓
EntityHealth 扣除生命并判断死亡
↓
EntityStatusHandler 添加击退、眩晕等效果
↓
事件通知 VFX、UI、音频
状态负责“何时攻击”,Combat 负责“造成什么结果”,Health 负责“生命怎样变化”,表现层负责“怎样反馈”。这条边界能让玩家和敌人复用结算,也便于以后替换伤害公式。
如果继续整理,我会先改什么
如果继续整理项目,我会按以下顺序推进:
- 给状态切换增加优先级、原因记录和关键测试;
- 抽出
PlayerStateContext,减少状态构造样板; - 将攻击与技能效果统一走 Combat/Effect 接口;
- 明确 Stat modifier 与 Buff 的叠加顺序;
- 把旧技能树迁移到统一 UI 和存档接口;
- 清理 Legacy 与编码问题,再补项目演示。
这个顺序先保证行为和结算正确,再整理内容制作工具。直接先重做技能树 UI,无法解决底层状态和数据边界问题。
写在最后
- 玩家和敌人共享实体能力与状态机机制,但不共享决策来源。
- Enemy 的 Battle 状态适合承接距离、冷却和攻击选择,具体攻击通过策略扩展。
Stat和 modifier 让属性修改有来源,Buff 不应直接乘除裸属性。- 技能需要分离配置、运行时状态和具体效果,UI 只提交意图。
- 项目说明需要标出已经验证的范围、废弃实现和后续方向。
MetroidvaniaLike 的三篇先写到这里。这个项目完成度不如商业项目,但状态机、动作手感、属性和技能几个部分都能继续往下整理。现阶段最需要补的是状态切换约束、旧技能树迁移和更完整的测试,等实际修改后再继续更新。