跳到主要内容

多角色队伍

Guide

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

GameEncounter 可以同时创建一名主角色和多名队伍成员,每个成员都保留独立的生命、护甲、状态、Hook、角色定义与场景表现,队伍默认共享一次玩家回合、同一组牌堆和能量池,因此现有单角色战斗可以继续使用原有配置,多角色战斗则只需要在同一套运行时上扩展阵容

上图呈现了 Encounter、队伍成员、共享回合与实际结算之间的完整工作链路,Encounter 先建立稳定的角色身份与槽位,全队共用卡牌和能量,每次成功出牌再记录卡牌原始身份、实际行动来源、完整目标、FlowGraph 上下文、事件与场景表现

功能边界

多角色队伍解决的是一场本地战斗中存在多名完整玩家单位的问题,它提供行动策略和出牌策略等玩法扩展边界,但不包含网络连接、状态同步、回滚、房间或账号系统

用 Encounter 建立初始队伍

Player Units 是完整的初始阵容。第 1 行保留旧项目使用的主角色引用,第 23 行保存追加成员;编辑器和运行时均固定最多三名玩家

配置运行结果
Player Units[0]创建槽位 0 的主角色;旧版 PlayerUnitGCSApi.Player() 仍解析该成员
Player Units[1..2]按行顺序创建额外的 PlayerUnitState
行顺序直接映射场景槽位 012,不需要额外配置槽位

Workbench 会报告空成员、超过三人、越界槽位与重复槽位。Encounter 预览把 Player Units 连续排列在 VS 左侧,进入 Play Mode 前即可核对双方阵容

角色身份贯穿整场战斗

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

身份作用
UnitId唯一标识本场战斗中的一个单位实例,同一 Player Unit 资产被创建两次时会得到两个不同值
Team / TeamId决定友军与敌军关系,内置玩家和敌人分别使用 PlayerEnemy
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 AllyOther AllyAll AlliesRandom AllyRandom OpponentAny 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 AlliesAll Opponents 都按当前 Source 的阵营相对解析,因此同一段图既可以挂在玩家卡牌上,也可以挂在敌人或状态上;Active Player 返回当前选中的存活玩家成员,All Dead Allies 则为复活规则提供待复活集合,完整目标关系见目标选择

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

Spawn UnitTeam 创建玩家或敌人。玩家增援受三人上限约束,成功后取得稳定槽位并发布 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);

已移除的身份不能再复活,重复处理同一次死亡也不会重复发布死亡事件,移除或替换成员不会拆分、迁移或重建全队共享的卡牌与能量;OnUnitRemovedOnUnitDied 分别表示离开阵容和留在阵容中等待复活

Demo BattleBoard 会订阅成员加入、单位移除、死亡、复活与当前角色变化,单角色场景只显示一个玩家槽位,第二或第三名玩家加入时,运行时在同一个 Player Area 根对象下展开子槽位,Party Showcase 直接保存三个子槽位,Dashboard 的 Initialize System 也会创建这套层级

在 Party Showcase 中验证完整链路

打开 Demo/Scenes/Multi-Character/Party Showcase 后进入 Play Mode,棋盘应同时显示 ScoutSquireApprenticePlague StalkerSpore BruteMire Reaper,底部只保留一组共享手牌和能量显示

Party Showcase 同时显示三名玩家、三名敌人、共享手牌与共享能量

Player Unit 1Scout 提供 Party Tactics,运行时据此创建全队共享的八张卡牌

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

完成这些检查后,Encounter、玩家输入、FlowGraph、场景表现与公开 API 都应指向同一组成员身份。单角色 Encounter 只保留一行 Player Units 即可