多角色队伍
让多名完整玩家角色共同进入一场战斗,并让每张卡牌、每次结算和每个表现对象始终指向正确角色
GameEncounter 可以同时创建一名主角色和多名队伍成员,每个成员都保留独立的生命、护甲、状态、Hook、角色定义与场景表现,队伍默认共享一次玩家回合、同一组牌堆和能量池,因此现有单角色战斗可以继续使用原有配置,多角色战斗则只需要在同一套运行时上扩展阵容
上图呈现了 Encounter、队伍成员、共享回合与实际结算之间的完整工作链路,Encounter 先建立稳定的角色身份与槽位,全队共用卡牌和能量,每次成功出牌再记录卡牌原始身份、实际行动来源、完整目标、FlowGraph 上下文、事件与场景表现
多角色队伍解决的是一场本地战斗中存在多名完整玩家单位的问题,它提供行动策略和出牌策略等玩法扩展边界,但不包含网络连接、状态同步、回滚、房间或账号系统
用 Encounter 建立初始队伍
Player Units 是完整的初始阵容。第 1 行保留旧项目使用的主角色引用,第 2、3 行保存追加成员;编辑器和运行时均固定最多三名玩家
| 配置 | 运行结果 |
|---|---|
Player Units[0] | 创建槽位 0 的主角色;旧版 PlayerUnit 和 GCSApi.Player() 仍解析该成员 |
Player Units[1..2] | 按行顺序创建额外的 PlayerUnitState |
| 行顺序 | 直接映射场景槽位 0、1、2,不需要额外配置槽位 |
Workbench 会报告空成员、超过三人、越界槽位与重复槽位。Encounter 预览把 Player Units 连续排列在 VS 左侧,进入 Play Mode 前即可核对双方阵容
角色身份贯穿整场战斗
每名运行时单位都同时保存 UnitId、Team、TeamId、SlotIndex 与 JoinOrder,这些字段分别承担实例寻址、阵营关系、画面位置与稳定顺序,不能用角色资产名或列表下标替代
| 身份 | 作用 |
|---|---|
UnitId | 唯一标识本场战斗中的一个单位实例,同一 Player Unit 资产被创建两次时会得到两个不同值 |
Team / TeamId | 决定友军与敌军关系,内置玩家和敌人分别使用 Player 与 Enemy |
SlotIndex | 把运行时单位映射到玩家或敌人侧的可见槽位 |
JoinOrder | 保留成员进入队伍的先后顺序,增援与主角色变更不会打乱其余成员 |
GCSApi.Player() 继续返回兼容主角色,GCSApi.ActivePlayer() 返回当前行动角色,GCSApi.PlayerParty() 返回包含待复活成员的稳定队伍,AlivePlayers() 与 DeadPlayers() 则把可行动成员和待复活成员分开,项目 UI 应根据实际职责选择入口,不能把兼容主角色当成完整队伍
一次玩家回合由整支队伍共同完成
默认战斗流程只创建一次玩家阶段,回合开始时会为所有成员处理护甲、状态与生命周期,再为全队刷新一次能量和抽牌;玩家可以在行动窗口内切换当前角色并打出合法卡牌,点击 End Turn 后,系统才会关闭整支队伍的行动窗口并进入敌人阶段
一名成员的 SkipNextTurns 只会阻止该成员成为当前行动角色,不会跳过其他可行动成员;只有全部存活成员都被跳过时,本次队伍回合才会直接结束。默认失败条件要求玩家侧所有成员都已死亡,因此主角色倒下不会提前结束仍可继续的战斗;默认胜利条件只会在 HasEncounteredEnemy 变为 true 后生效,因此 Encounter 可以先以空敌方阵容开始,再于运行时生成首名敌人
需要固定行动顺序时,可以通过 BattlePolicySet.ActivationPolicy 使用 OrderedPartyActivationPolicy,随后用 EndActivePlayerActivation() 推进到下一名成员。把 BattlePolicySet 传给 GCSApi.StartBattle 后,GCS 会在创建单位和卡牌前补齐所有默认策略,并把最终策略集冻结到本场战斗;需要改变规则时应为下一场战斗创建新的策略集,不能在 Play Mode 中修改 ActivePolicies
| 策略 | 决定的规则 |
|---|---|
IPartyActivationPolicy | 自由选择或按顺序激活成员 |
IAutomaticPartyActivationPolicy | 在顺序回合中自动执行同伴行动 |
IActionWindowPolicy | 哪些阶段接受玩家出牌请求 |
IReactionWindowPolicy | 敌方行动前是否打开可暂停的反应窗口 |
ICardPlayPolicy | 项目自己的行动来源或卡牌许可 |
IUnitTargetPolicy | 不依赖画面选中对象,为行动者解析默认敌对目标 |
IBattleResultPolicy | 当前状态是否进入胜利或失败阶段 |
全队共享卡牌与能量
所有玩家成员共用一份 Hand、Draw、Discard、Exhaust 和当前能量。起手、回合抽牌、洗牌、手牌上限、能量变化与卡牌移动都只为整支队伍结算一次,不会按成员重复执行
持久 Master Deck 为空时,Player Unit 1 的 Starting Deck 用于建立共享 Draw 牌堆。后续行角色配置的 Starting Deck 不会合并进当前战斗;当这些 Player 资产在其他 Encounter 中成为 Player Unit 1 时,仍可使用自己的卡组。战斗卡牌保留主角色的 OwnerUnitId 作为不可变来源记录,但之后可以由当前选中的任意合格成员打出
当前角色、卡牌所有者与结算来源彼此独立
旧版兼容入口 TryPlayCard(card, target) 始终使用当前 ActivePlayer() 作为本次行动来源,这个来源决定 Self、相对来源 Hook、FlowGraph 执行上下文与出牌事件。OwnerUnitId 仍然只表示卡牌的原始内容来源,切换当前角色或执行卡牌都不会改写它。需要让顺序行动成员或自动同伴明确执行一张卡牌时,使用 CardPlayRequest 同时提交合格的 SourceUnit 和目标
PlayerUnitState member = GCSApi.AlivePlayers()[1] as PlayerUnitState;
GCSApi.TrySetActivePlayerUnit(member);
foreach (CardInstance card in GCSApi.Hand(member))
{
var request = new CardPlayRequest(card, member, GCSApi.FirstAliveEnemy());
int cost = GCSApi.GetEffectiveEnergyCost(card);
int energy = GCSApi.Energy(member);
if (cost <= energy && GCSApi.CanPlayCard(request))
{
GCSApi.TryPlayCard(request);
}
}
一次出牌被接受后,CardInstance.LastPlayContext 会保存不可变的 CardPlayContext,其中包含出牌前区域、原始所有者、实际来源和完整目标列表。OnCardPlayed 提供同样的身份划分,自定义 UI、统计或外部系统不需要再从卡牌所有权猜测实际执行者
卡牌可以选择友军并按相对阵营结算
Card Target 除了原有敌方目标外,还提供 Single Ally、Other Ally、All Allies、Random Ally、Random Opponent 与 Any Unit,单体友军和任意单位输入会在 Demo 中使用同一套拖拽箭头与合法目标高亮,范围和随机目标则由运行时自动解析
| Card Target | 玩家输入 | Behavior 的对应来源 |
|---|---|---|
Single Ally | 选择任意一名存活友军,允许选择来源角色 | Opponent 不适用,使用已提交 Target 或目标端口 |
Other Ally | 选择来源角色以外的存活友军 | 使用已提交 Target,运行时会拒绝来源角色本身 |
All Allies | 不需要单体输入 | All Allies |
Random Ally | 不需要单体输入 | 运行时从全部存活友军中选择一名,并通过 Target 交给 Behavior |
Random Opponent | 不需要单体输入 | 运行时从来源角色的对方阵营选择一名存活单位,并通过 Target 交给 Behavior |
Any Unit | 选择任意一名存活玩家或敌人 | 使用已提交 Target,不限制双方阵营 |
FlowGraph 中的 All Allies 与 All Opponents 都按当前 Source 的阵营相对解析,因此同一段图既可以挂在玩家卡牌上,也可以挂在敌人或状态上;Active Player 返回当前选中的存活玩家成员,All Dead Allies 则为复活规则提供待复活集合,完整目标关系见目标选择
运行时增援、离队、死亡与复活使用同一份阵容
Spawn Unit 按 Team 创建玩家或敌人。玩家增援受三人上限约束,成功后取得稳定槽位并发布 OnPlayerUnitAdded,不会创建新的牌堆或能量。ReplacePlayer 在同一槽位创建替代成员,共享卡牌与能量保持不变;Despawn Unit 移除目标并发布 OnUnitRemoved,普通死亡只发布 OnUnitDied 并保留成员与槽位,Revive Unit 恢复该成员后发布 OnUnitRevived
PlayerUnitState joined = GCSApi.SummonPlayer(playerDefinition, 2, GCSApi.ActivePlayer());
if (joined != null)
{
GCSApi.SetEnergy(joined, GCSApi.MaxEnergyFor(joined));
}
GCSApi.RemoveUnit(joined);
已移除的身份不能再复活,重复处理同一次死亡也不会重复发布死亡事件,移除或替换成员不会拆分、迁移或重建全队共享的卡牌与能量;OnUnitRemoved 与 OnUnitDied 分别表示离开阵容和留在阵容中等待复活
Demo BattleBoard 会订阅成员加入、单位移除、死亡、复活与当前角色变化,单角色场景只显示一个玩家槽位,第二或第三名玩家加入时,运行时在同一个 Player Area 根对象下展开子槽位,Party Showcase 直接保存三个子槽位,Dashboard 的 Initialize System 也会创建这套层级
在 Party Showcase 中验证完整链路
打开 Demo/Scenes/Multi-Character/Party Showcase 后进入 Play Mode,棋盘应同时显示 Scout、Squire、Apprentice、Plague Stalker、Spore Brute 与 Mire Reaper,底部只保留一组共享手牌和能量显示

Player Unit 1 的 Scout 提供 Party Tactics,运行时据此创建全队共享的八张卡牌
- 确认三个玩家槽位分别显示生命、护甲与状态区域
- 点击不同成员,确认当前角色与能量显示同步变化
- 切换成员后打出共享
Party Tactics中的攻击牌,确认费用由同一份队伍能量支付、当前成员成为SourceUnit,卡牌原始OwnerUnitId保持不变 - 依次使用
Field Mend、Interpose、Shared Resolve与Emergency Aid,确认指定队友、排除自身、全体队友与随机队友四条目标路径 - 结束队伍回合,确认所有成员完成回合生命周期后敌人才开始行动
- 打开 Game Card Monitor,核对每名成员的
UnitId、槽位、当前行动状态、共享能量与状态
完成这些检查后,Encounter、玩家输入、FlowGraph、场景表现与公开 API 都应指向同一组成员身份。单角色 Encounter 只保留一行 Player Units 即可