跳到主要内容

多角色队伍

Guide

让多名完整玩家角色共同进入一场战斗,并让每张卡牌、每次结算和每个表现对象始终指向正确角色

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 前直接核对

角色身份贯穿整场战斗

每名运行时单位都同时保存 UnitIdTeamTeamIdControllerIdSlotIndexJoinOrder,这些字段分别承担实例寻址、阵营关系、控制权、画面位置与稳定顺序,不能用角色资产名或列表下标替代

身份作用
UnitId唯一标识本场战斗中的一个单位实例,同一 Player Unit 资产被创建两次时会得到两个不同值
Team / TeamId决定友军与敌军关系,内置玩家和敌人分别使用 PlayerEnemy
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全部成员使用一个队伍资源池,起手、回合抽牌与能量刷新各执行一次一次玩家阶段中自由选择角色并共同使用手牌
PerControllerControllerId 相同的成员共享资源,不同控制者各自拥有牌堆与能量本地协作输入或由不同逻辑控制器管理的成员组
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 与全部卡牌生命周期事件都会同时提供 OwnerUnitIdSourceUnitIdControllerIdResourceOwner,自定义 UI、统计或外部系统可以据此区分卡牌原始所有者、实际执行者与支付费用的资源池

卡牌可以选择友军并按相对阵营结算

Card Target 除了原有敌方目标外,还提供 Single AllyOther AllyAll AlliesRandom Ally,前两类单体输入会在 Demo 中使用同一套拖拽箭头与合法目标高亮,范围和随机目标则由运行时自动解析

Card Target玩家输入Behavior 的对应来源
Single Ally选择任意一名存活友军,允许选择来源角色Opponent 不适用,使用已提交 Target 或目标端口
Other Ally选择来源角色以外的存活友军使用已提交 Target,运行时会拒绝来源角色本身
All Allies不需要单体输入All Allies
Random Ally不需要单体输入运行时从全部存活友军中选择一名,并通过 Target 交给 Behavior

FlowGraph 中的 All AlliesAll Opponents 都按当前 Source 的阵营相对解析,因此同一段图既可以挂在玩家卡牌上,也可以挂在敌人或状态上;Active Player 返回当前选中的存活玩家成员,All Dead Allies 则为复活规则提供待复活集合,完整目标关系见目标选择

运行时增援、离队、死亡与复活使用同一份阵容

Spawn Unit 可以按 Team 创建玩家或敌人,玩家增援会检查 Player Capacity、分配槽位、初始化对应资源池并发布 OnPlayerUnitAddedDespawn 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 SquireParty ApprenticeParty Acolyte 建立三人队伍,并让他们共同对战两名敌人

  1. 确认三个玩家槽位分别显示生命、护甲与状态区域
  2. 点击不同成员,确认当前角色与能量显示同步变化
  3. 打出共享 Party Tactics 中的攻击牌,确认费用由同一个队伍资源池支付,但卡牌来源仍保留对应成员身份
  4. 依次使用 Field MendInterposeShared ResolveEmergency Aid,确认指定队友、排除自身、全体队友与随机队友四条目标路径
  5. 结束队伍回合,确认所有成员完成回合生命周期后敌人才开始行动
  6. 打开 Game Card Monitor,核对每名成员的 UnitId、槽位、控制者、资源池与状态

完成这些检查后,Encounter 配置、玩家输入、FlowGraph 结算、场景表现和公开 API 已经沿同一份队伍身份闭环运行,单角色项目则可以保持 Party Members 为空,继续沿用原有场景与代码