多角色队伍
让多名完整玩家角色共同进入一场战斗,并让每张卡牌、每次结算和每个表现对象始终指向正确角色
GameEncounter 可以同时创建一名主角色和多名队伍成员,每个成员都保留独立的生命、护甲、状态、Hook、角色定义与场景表现,队伍默认共享一次玩家回合、同一组牌堆和能量池,因此现有单角色战斗可以继续使用原有配置,多角色战斗则只需要在同一套运行时上扩展阵容
上图呈现了 Encounter、队伍成员、共享回合、资源归属与实际结算之间的完整工作链路,Encounter 先建立稳定的角色身份与槽位,回合和资源策略再决定每名成员能够使用哪些卡牌与能量,最终由卡牌来源、目标规则和场景表现把同一份身份贯穿到玩家操作与运行结果中
多角色队伍解决的是一场本地战斗中存在多名完整玩家单位的问题,它提供 ControllerId、资源策略、行动策略和出牌策略等扩展边界,但不包含网络连接、状态同步、回滚、房间或账号系统
用 Encounter 建立初始队伍
Encounter 的 Player 仍然保存主角色,这项引用同时承担旧项目的兼容入口;Player Capacity 决定玩家侧最多可以容纳多少名单位,Party Members 按成员配置补充初始队伍,Resource Mode 则决定这些成员怎样共享或分离牌堆与能量
| 配置 | 运行结果 |
|---|---|
Player Unit | 创建主角色并占用槽位 0,旧代码中的 Player() 与 PlayerUnit 继续返回这名角色,主角色离队后会自动指向仍在队伍中的第一名成员 |
Player Capacity | 限制初始成员与运行时增援的总数,Demo 棋盘默认提供 3 个可见玩家槽位 |
Party Members[].Unit | 为每一项创建独立的 PlayerUnitState,角色从自己的 Player Unit 定义读取生命、基础能量和 Starting Deck |
Party Members[].ControllerId | 标识负责该角色的逻辑控制者,未填写时使用默认玩家控制者 |
Party Members[].SlotIndex | 指定稳定的场景槽位,-1 会自动选择空位,显式槽位不能与主角色或其他成员重复 |
Resource Mode | 选择共享队伍、按控制者或按角色划分牌堆与能量 |
队伍规模不能超过 Player Capacity,Workbench 校验还会检查空成员、越界槽位与重复槽位,完成配置后,Encounter 预览会把主角色与全部队伍成员连续放在 VS 左侧,使玩家阵容与敌人阵容可以在进入 Play Mode 前直接核对
角色身份贯穿整场战斗
每名运行时单位都同时保存 UnitId、Team、TeamId、ControllerId、SlotIndex 与 JoinOrder,这些字段分别承担实例寻址、阵营关系、控制权、画面位置与稳定顺序,不能用角色资产名或列表下标替代
| 身份 | 作用 |
|---|---|
UnitId | 唯一标识本场战斗中的一个单位实例,同一 Player Unit 资产被创建两次时会得到两个不同值 |
Team / TeamId | 决定友军与敌军关系,内置玩家和敌人分别使用 Player 与 Enemy |
ControllerId | 为按控制者划分资源、输入与自定义回合策略提供稳定键值 |
SlotIndex | 把运行时单位映射到玩家或敌人侧的可见槽位 |
JoinOrder | 保留成员进入队伍的先后顺序,增援与主角色变更不会打乱其余成员 |
GCSApi.Player() 继续返回兼容主角色,GCSApi.ActivePlayer() 返回当前行动角色,GCSApi.PlayerParty() 返回包含待复活成员的稳定队伍,AlivePlayers() 与 DeadPlayers() 则把可行动成员和待复活成员分开,项目 UI 应根据实际职责选择入口,不能把兼容主角色当成完整队伍
一次玩家回合由整支队伍共同完成
默认战斗流程只创建一次玩家阶段,回合开始时会为所有成员处理护甲、状态与生命周期,再按资源池刷新能量和抽牌;玩家可以在行动窗口内切换当前角色并打出合法卡牌,点击 End Turn 后,系统才会关闭整支队伍的行动窗口并进入敌人阶段
一名成员的 SkipNextTurns 只会阻止该成员成为当前行动角色,不会跳过其他可行动成员;只有全部存活成员都被跳过时,本次队伍回合才会直接结束,默认失败条件同样要求玩家侧所有成员都已死亡,因此主角色倒下不会提前结束仍可继续的战斗
需要固定行动顺序时,可以把 BattleStateMachine.ActivationPolicy 替换为 OrderedPartyActivationPolicy,随后用 EndActivePlayerActivation() 推进到下一名成员;项目还可以通过 IPartyActivationPolicy 控制成员激活方式、通过 IActionWindowPolicy 限定出牌阶段,并用 ICardPlayPolicy 增加项目自己的出牌许可,而不必修改伤害、状态、卡牌或 FlowGraph 执行器
资源策略决定手牌与能量属于谁
Resource Mode 会把每名玩家单位映射为一个 PartyResourceOwner,同一个资源所有者共享手牌、抽牌堆、弃牌堆、消耗堆与能量,不同资源所有者则拥有彼此隔离的牌堆和能量
| 模式 | 牌堆与能量 | 适用结构 |
|---|---|---|
SharedParty | 全部成员使用一个队伍资源池,起手、回合抽牌与能量刷新各执行一次 | 一次玩家阶段中自由选择角色并共同使用手牌 |
PerController | ControllerId 相同的成员共享资源,不同控制者各自拥有牌堆与能量 | 本地协作输入或由不同逻辑控制器管理的成员组 |
PerUnit | 每名成员使用独立牌堆和能量 | 每名角色拥有独立构筑与资源预算的队伍 |
角色 Starting Deck 中创建的卡牌会保存该角色的 OwnerUnitId,牌堆位置则由 ResourceOwner 决定;共享模式会把各成员的 Starting Deck 合并进同一个资源池,但每张卡牌仍保留原始所有者,因此卡牌执行时可以准确使用对应角色的状态、Hook、Controller 与 Self 目标
没有角色所有者的临时卡牌会使用当前行动角色作为来源,持有者已经死亡但仍留在队伍中的卡牌会回退到当前存活成员执行,成员被移出阵容后,其最后一个独立资源池中的卡牌会转入剩余队伍的有效资源池,同时保留原始 OwnerUnitId 作为历史身份
当前角色、卡牌所有者与结算来源彼此独立
当前角色决定 ownerless 卡牌和当前 UI 默认显示哪个资源池,卡牌所有者决定一张已有卡牌默认由谁执行,目标则决定这次行为影响谁,三者使用不同字段保存,切换当前角色不会篡改已有卡牌的所有权
PlayerUnitState member = GCSApi.AlivePlayers()[1] as PlayerUnitState;
GCSApi.TrySetActivePlayerUnit(member);
foreach (CardInstance card in GCSApi.Hand(member))
{
int cost = GCSApi.GetEffectiveEnergyCost(card);
int energy = GCSApi.Energy(member);
if (cost <= energy && GCSApi.CanPlayCard(card))
{
// 根据 RequiresTarget 的结果继续提交目标
}
}
OnCardPlayed 与全部卡牌生命周期事件都会同时提供 OwnerUnitId、SourceUnitId、ControllerId 与 ResourceOwner,自定义 UI、统计或外部系统可以据此区分卡牌原始所有者、实际执行者与支付费用的资源池
卡牌可以选择友军并按相对阵营结算
Card Target 除了原有敌方目标外,还提供 Single Ally、Other Ally、All Allies 与 Random Ally,前两类单体输入会在 Demo 中使用同一套拖拽箭头与合法目标高亮,范围和随机目标则由运行时自动解析
| Card Target | 玩家输入 | Behavior 的对应来源 |
|---|---|---|
Single Ally | 选择任意一名存活友军,允许选择来源角色 | Opponent 不适用,使用已提交 Target 或目标端口 |
Other Ally | 选择来源角色以外的存活友军 | 使用已提交 Target,运行时会拒绝来源角色本身 |
All Allies | 不需要单体输入 | All Allies |
Random Ally | 不需要单体输入 | 运行时从全部存活友军中选择一名,并通过 Target 交给 Behavior |
FlowGraph 中的 All Allies 与 All Opponents 都按当前 Source 的阵营相对解析,因此同一段图既可以挂在玩家卡牌上,也可以挂在敌人或状态上;Active Player 返回当前选中的存活玩家成员,All Dead Allies 则为复活规则提供待复活集合,完整目标关系见目标选择
运行时增援、离队、死亡与复活使用同一份阵容
Spawn Unit 可以按 Team 创建玩家或敌人,玩家增援会检查 Player Capacity、分配槽位、初始化对应资源池并发布 OnPlayerUnitAdded;Despawn Unit 会从任一阵营移除目标并发布 OnUnitRemoved,普通死亡仍保留成员与槽位以等待复活,Revive Unit 可以通过 All Dead Allies 恢复同阵营成员
PlayerUnitState joined = GCSApi.SummonPlayer(playerDefinition, "local-2", 2, GCSApi.ActivePlayer());
if (joined != null)
{
GCSApi.SetEnergy(joined, GCSApi.MaxEnergyFor(joined));
}
GCSApi.RemoveUnit(joined);
Demo BattleBoard 会订阅成员加入、单位移除、死亡、复活与当前角色变化,旧场景只保存一个 PlayerArea 时会自动扩展为可复用的三个玩家槽位,Dashboard 的 Initialize System 也会把相同槽位结构写入当前场景,因此新场景与已有场景使用同一套初始化结果
在 Party Showcase 中验证完整链路
打开 Demo/Scenes/Party/Party Showcase 后进入 Play Mode,场景会用 Party Squire、Party Apprentice 与 Party Acolyte 建立三人队伍,并让他们共同对战两名敌人
- 确认三个玩家槽位分别显示生命、护甲与状态区域
- 点击不同成员,确认当前角色与能量显示同步变化
- 打出共享
Party Tactics中的攻击牌,确认费用由同一个队伍资源池支付,但卡牌来源仍保留对应成员身份 - 依次使用
Field Mend、Interpose、Shared Resolve与Emergency Aid,确认指定队友、排除自身、全体队友与随机队友四条目标路径 - 结束队伍回合,确认所有成员完成回合生命周期后敌人才开始行动
- 打开 Game Card Monitor,核对每名成员的
UnitId、槽位、控制者、资源池与状态
完成这些检查后,Encounter 配置、玩家输入、FlowGraph 结算、场景表现和公开 API 已经沿同一份队伍身份闭环运行,单角色项目则可以保持 Party Members 为空,继续沿用原有场景与代码