14 KiB
角色行为(技能)系统设计
目标
实现一套大厅角色和小游戏都能复用的角色行为系统。行为通过 JSON 配置,可描述技能、动作、特效、飞行物、Buff 和逻辑效果。客户端和服务端加载同一套 JSON 文件;服务端只运行权威逻辑,客户端消费表现事件以及服务端同步来的权威结果。
第一版范围是共享核心、示例配置和自动化测试,不直接接入大厅角色控制器,也不直接接入某个小游戏 UI。
当前项目背景
- Unity 客户端共享框架代码位于
Client/Assets/Framework/Shared。 Server/Framework.Shared/Framework.Shared.csproj会从客户端共享目录编译同一份源码。- 小游戏 Core 代码也使用同源模式:
scripts~目录同时被客户端和服务端项目编译。 - 已有虚拟 HUD 输入系统只负责输出摇杆和按钮事件,不负责技能协议或技能执行。角色行为系统位于输入层之后,把输入转换为行为请求。
- 通用行为配置放在
Client/Assets/Game/Config/ActorBehavior;小游戏私有行为配置放在对应小游戏目录下的Config/ActorBehavior,例如Client/Assets/MiniGames/RockPaperScissors/Config/ActorBehavior。服务端需要通过文件或字符串读取同一套 JSON 内容。
推荐方案
新增一个纯 C# 共享模块:
Client/Assets/Framework/Shared/ActorBehavior/
命名空间:
XWorld.Framework.ActorBehavior
共享核心不引用 Unity。它负责配置模型、确定性的运行时状态、时间线触发、飞行物移动、Buff tick、逻辑效果目标选择和结构化事件输出。
Unity 相关逻辑放在核心之外。客户端后续用适配层把 TimelineAnimation、TimelineEffect、飞行物表现事件接到 Animator、资源加载和特效预设。服务端后续用适配层把权威逻辑事件接到真实游戏属性、快照和网络广播。
备选方案
共享核心优先
这是选定方案。它符合当前项目的共享框架设计,保证客户端和服务端使用同一套行为规则,也方便大厅和小游戏后续同时接入。
取舍:第一版通过测试和示例配置证明系统可用,不会先做一个可见的大厅技能效果。
每个小游戏各自实现行为 Core
每个小游戏都可以打包自己的行为运行时。
优点是小游戏隔离更强;缺点是 fighter、mage 等通用人物配置会被复制多份,大厅也难以复用。
Unity 客户端优先
也可以先在客户端用 Unity 动画、Prefab、碰撞 API 直接实现技能,再补服务端。
优点是能较快看到视觉效果;缺点是规则会绑定 Unity 对象生命周期,后续补服务端权威逻辑会变得很难。
核心组件
BehaviorConfigCatalog
表示一种角色类型的一整套配置,例如 fighter。
输入文件:
fighter_skill.jsonfighter_flyobj.jsonfighter_buff.json
职责:
- 解析 JSON 字符串。
- 校验重复 id、缺失引用、非法时长、非法形状参数和不支持的枚举值。
- 按 id 提供行为、飞行物、Buff 和逻辑效果定义。
BehaviorWorld
管理多个角色和飞行物的运行时模拟。
职责:
- 注册多套
BehaviorConfigCatalog。 - 添加、移除、更新角色运行时状态。
- 按角色类型切换角色当前使用的配置。
- 开始和停止行为。
- Tick 当前行为时间线。
- Tick 飞行物移动和碰撞。
- Tick Buff 持续时间、起效间隔和重复次数。
- 输出并清空本帧产生的
BehaviorEvent。
主要 API 形态:
var catalog = BehaviorConfigCatalog.LoadFromJson(type, skillJson, flyObjJson, buffJson);
var world = new BehaviorWorld(new BehaviorWorldOptions { Mode = BehaviorRunMode.ServerLogic });
world.RegisterCatalog(catalog);
world.AddActor(actorState);
world.TryStartBehavior(actorId, "slash", BehaviorInput.FromDirection(x, z));
world.Tick(deltaTime);
IReadOnlyList<BehaviorEvent> events = world.DrainEvents();
ActorBehaviorController
表示 BehaviorWorld 内某一个角色的行为状态。
职责:
- 记录角色当前使用的配置类型。
- 记录当前主动行为和行为已运行时间。
- 保证同一角色同一时刻只能运行一个主动行为。
- 当前行为允许中断时,支持外部特殊逻辑显式打断。
- 管理附加在该角色身上的 Buff。
行为控制示例:
world.TryStartBehavior(actorId, "slash", BehaviorInput.FromDirection(1f, 0f));
world.StopBehavior(actorId, BehaviorStopReason.Stunned);
world.TryStartBehavior(actorId, "stun_fall", BehaviorInput.None);
飞行物运行时
飞行物是按 id 配置、独立存在的运行时对象。
职责:
- 由行为时间线或逻辑效果生成。
- 按配置的位移方式更新位置。
- 按配置的碰撞形状检测命中。
- 运行飞行物自己的时间线。
- 在碰撞或配置时间点触发逻辑效果。
第一版位移方式:
Line:直线飞行。Parabola:抛物线物理飞行。Homing:追踪目标。
第一版碰撞方式:
- 球形碰撞,包含半径和命中次数上限。
Buff 运行时
Buff 是附加在角色上的特殊效果。
职责:
- 记录持续时间。
- 按起效间隔 tick。
- 支持重复次数。
- 支持堆叠规则。
- 在添加、tick 或移除时触发配置的逻辑效果。
第一版堆叠规则:
RefreshDuration:已有同 id Buff 时刷新持续时间。IgnoreIfExists:已有同 id Buff 时忽略新 Buff。StackIndependent:同 id Buff 独立叠加。
逻辑效果运行时
逻辑效果负责按形状选取目标,并请求一个或多个业务效果。
第一版形状:
Sector:扇形。Sphere:球形。Ring:环形。Line:直线或胶囊范围。Locked:锁定目标。
第一版效果操作:
Attribute:请求属性变化,例如hp = -30。Buff:请求添加某个 Buff。Projectile:请求生成某个飞行物。
核心系统不维护真实业务属性表。它输出结构化事件,或通过适配器回调,让大厅或小游戏自行决定如何处理 hp、moveSpeed、score 或自定义属性。
JSON 配置
每种角色类型有三份配置文件,文件名前缀使用角色类型名:
fighter_skill.json
fighter_flyobj.json
fighter_buff.json
通用大厅/全局角色配置放在:
Client/Assets/Game/Config/ActorBehavior/
小游戏独有角色配置放在:
Client/Assets/MiniGames/<GameName>/Config/ActorBehavior/
所有文件都包含:
type:角色类型名,例如fighter。version:整数配置版本。
行为配置
{
"type": "fighter",
"version": 1,
"behaviors": [
{
"id": "slash",
"duration": 0.8,
"canBeInterrupted": true,
"input": "DirectionOrTarget",
"timeline": [
{ "time": 0.0, "kind": "Animation", "animation": "slash_01", "duration": 0.8 },
{ "time": 0.12, "kind": "Effect", "prefab": "Assets/Game/Art/Effect/slash.prefab", "duration": 0.5 },
{ "time": 0.25, "kind": "LogicEffect", "effectId": "slash_hit" },
{ "time": 0.32, "kind": "Projectile", "flyObjectId": "blade_wave" }
],
"logicEffects": [
{
"id": "slash_hit",
"shape": "Sector",
"radius": 2.4,
"angle": 90,
"target": "Enemy",
"effects": [
{ "type": "Attribute", "attribute": "hp", "value": -30 }
]
}
]
}
]
}
飞行物配置
{
"type": "fighter",
"version": 1,
"flyObjects": [
{
"id": "blade_wave",
"duration": 1.5,
"movement": { "kind": "Line", "speed": 6.0 },
"collision": { "shape": "Sphere", "radius": 0.4, "hitLimit": 1 },
"timeline": [
{ "time": 0.0, "kind": "Effect", "prefab": "Assets/Game/Art/Effect/blade_wave.prefab", "duration": 1.5 },
{ "time": 0.0, "kind": "LogicEffect", "effectId": "blade_wave_hit", "trigger": "OnCollision" }
],
"logicEffects": [
{
"id": "blade_wave_hit",
"shape": "Locked",
"target": "HitActor",
"effects": [
{ "type": "Attribute", "attribute": "hp", "value": -20 },
{ "type": "Buff", "buffId": "slow_01" }
]
}
]
}
]
}
Buff 配置
{
"type": "fighter",
"version": 1,
"buffs": [
{
"id": "slow_01",
"duration": 3.0,
"tickInterval": 1.0,
"repeatCount": 3,
"stacking": "RefreshDuration",
"logicEffects": [
{
"id": "slow_tick",
"shape": "Locked",
"target": "Owner",
"effects": [
{ "type": "Attribute", "attribute": "moveSpeed", "value": -0.2 }
]
}
]
}
]
}
输入模型
BehaviorInput 支持:
None:无输入。Direction:方向输入。TargetPosition:目标位置。TargetActor:锁定目标角色。
已有虚拟摇杆和动作按钮后续可把原始输入事件映射到这个输入模型。
示例:
- 摇杆方向加普攻按钮映射为
Direction。 - 拖拽释放技能映射为
Direction或TargetPosition。 - 锁定目标技能映射为
TargetActor。
运行模式
ServerLogic
服务端模式运行权威逻辑:
- 行为开始和停止。
- 时间线逻辑事件。
- 飞行物移动和碰撞。
- Buff 添加、tick 和移除。
- 逻辑效果目标选择。
- 属性、Buff 和飞行物请求。
该模式不执行动画和特效表现。它可以产出表现事件用于日志或可选网络转发,但这些事件本身不作为权威判定。
ClientPresentation
客户端表现模式消费行为状态和事件,用于播放表现:
- 动画时间线事件。
- 特效时间线事件。
- 飞行物表现生成和移动。
- Buff 表现添加和移除标记。
该模式不执行权威目标选择,也不执行属性结算。
ClientPredict
客户端预测可在核心稳定后继续扩展。它可以本地运行同一套时间线和移动逻辑,再根据服务端事件或快照校正。
第一版可以定义这个枚举值,但不需要实现完整校正流程。
事件模型
核心系统输出 BehaviorEvent。事件是确定性数据记录,不是对 Unity 的回调。
第一版事件类型:
BehaviorStartedBehaviorStoppedTimelineAnimationTimelineEffectProjectileSpawnedProjectileMovedProjectileHitBuffAddedBuffTickedBuffRemovedLogicEffectAppliedAttributeEffectRequested
服务端和客户端消费同一事件流,但职责不同:
- 客户端消费
TimelineAnimation、TimelineEffect和飞行物表现事件。 - 服务端消费
LogicEffectApplied、AttributeEffectRequested、Buff 事件和飞行物生成/命中事件。 - 网络接入后,服务端会把权威事件和必要快照广播给客户端。
目标选择与过滤
核心系统记录角色的最小确定性状态:
- 角色 id。
- 配置类型。
- 位置。
- 朝向。
- 队伍或阵营 id。
- 是否存活或启用。
- 可选半径,用于碰撞和目标选择。
逻辑效果目标过滤:
Self:自己。Ally:友方。Enemy:敌方。All:全部。Owner:效果拥有者。HitActor:被飞行物命中的角色。
第一版过滤规则保持简单、确定、可测试。更复杂的玩法规则后续通过适配器决定。
错误处理
配置加载遇到以下问题时失败,并返回清晰的校验信息:
- 缺少
type。 - skill、flyobj、buff 三份 JSON 的
type不一致。 - 行为、飞行物、Buff 或逻辑效果 id 重复。
- 时间线条目时间为负数。
- 时间线引用不存在的逻辑效果 id。
- 行为引用不存在的飞行物 id。
- 逻辑操作引用不存在的飞行物或 Buff id。
- 不支持的枚举字符串。
- 非法形状参数,例如半径为负数,或扇形角度不在
(0, 360]。
运行时行为:
- 开始未知行为会返回失败,不产生事件。
- 当前已有主动行为时,开始第二个行为会失败,除非当前行为已被显式停止或中断。
- 不可中断行为会忽略特殊停止请求,但
Death这类终止原因例外。 - 飞行物在持续时间结束或达到命中上限时过期。
- Buff 在持续时间结束或达到重复次数时过期。
测试
在 Server/Framework.Shared.Tests 添加 xUnit 测试。
必须覆盖:
- 能从三段 JSON 字符串加载完整
fighter配置。 - 拒绝三份配置
type不一致。 - 拒绝重复 id 和缺失引用。
- 同一角色同一时刻只能运行一个主动行为。
- 当前行为可中断时,允许特殊停止并切换到另一个行为。
- 时间线能按配置时间触发动画、特效、逻辑效果和飞行物条目。
- 扇形、球形、环形、直线、锁定形状能正确选择目标。
- 直线、抛物线、追踪飞行物能更新位置。
- 飞行物球形碰撞能触发命中,并在达到命中上限后过期。
- Buff 能按持续时间、起效间隔和重复次数 tick。
- 逻辑效果能产出属性、Buff、飞行物请求。
ServerLogic模式会产出权威逻辑事件,并跳过表现执行。ClientPresentation模式会产出表现事件,并跳过权威属性请求。
验收标准
- 调用方能从
fighter_skill.json、fighter_flyobj.json、fighter_buff.json加载一套fighter行为配置。 BehaviorWorld能注册多套角色类型配置。- 每个角色能按类型名切换当前行为配置。
- 调用方能通过角色 id、行为 id 和方向或目标输入开始行为。
- 同一角色不能同时运行两个主动行为。
- 外部特殊逻辑能在规则允许时停止或中断行为,并切换到其他行为。
- 行为时间线能产出动画、特效、飞行物、Buff 和逻辑事件。
- 飞行物能独立移动,并在碰撞时触发逻辑。
- Buff 能附加到角色、重复 tick 并过期。
- 逻辑效果支持扇形、球形、环形、直线和锁定目标选择。
- 服务端逻辑运行不依赖 Unity 动画、Prefab 或特效。
- 客户端后续能消费同一事件流,用于播放动画、加载预设和表现飞行物。