卡牌与牌堆生命周期
理解 CardInstance 的创建、抽取、打出、保留、弃置与消耗,以及 Run Master Deck 和战斗牌堆的边界
卡牌进入战斗后会从可复用的 GameCard 定义转为独立 CardInstance,再随着抽牌、出牌与回合结束在四个战斗牌堆之间移动,Game Card Monitor 的 Pile 标签页可以直接核对每张实例此刻所在的位置

上图中的 Hand、Draw、Discard 与 Exhaust 共同组成当前战斗的卡牌循环,Master 则属于跨战斗保留的 Run 数据,理解这些牌堆前,需要先分清创作定义与战斗实例
卡牌定义与战斗实例
GameCard 是可复用的创作定义,保存名称、说明、基础费用、分类、Target、美术与 Behavior,CardInstance 是当前战斗中的一张具体卡牌,额外记录实例 ID、升级状态和实例费用变化,内容浏览、数据库查找和卡组配置请使用 GameCard,而手牌显示、出牌、移动和临时修改则使用 CardInstance
| 运行时结果 | API |
|---|---|
| 当前生效的基础卡或升级卡 | GCSApi.GetActiveCard(instance) |
| 经过 Run 修正、临时规则与 Hook 后的费用 | GCSApi.GetEffectiveEnergyCost(instance) |
| 当前卡面说明 | GCSApi.FormatDescription(card, instance) |
四个战斗牌堆
| 牌堆 | 内容 | 读取方式 |
|---|---|---|
| Hand | 玩家当前可以操作的手牌 | GCSApi.Hand() |
| Draw | 等待抽取的卡牌 | GCSApi.DrawPile() |
| Discard | 可以重新洗回抽牌堆的弃牌 | GCSApi.DiscardPile() |
| Exhaust | 本场战斗中已经移出常规循环的卡牌 | GCSApi.ExhaustPile() |
上图展示了默认牌堆循环,卡牌也可以由 FlowGraph 或 GCSApi 直接创建、移动或移除,但所有变化仍应经过受支持的运行时入口
Hand()、DrawPile()、DiscardPile()和ExhaustPile()没有活动战斗时会返回空列表,不会返回null- 直接使用
GCSApi.CurrentHand来获取暴露到 Manager 的当前手牌,在战斗外可能为null,因此,普通 UI 更适合使用Hand()
抽牌顺序与洗牌
抽牌堆使用列表末尾作为堆顶,也就是下一张被抽到的卡牌,向 Draw 牌堆插入新卡时,DrawPilePosition 决定位置
| 位置 | 结果 |
|---|---|
Top | 先于当前牌堆内容抽到 |
Bottom | 晚于当前牌堆内容抽到 |
Random | 插入随机位置 |
抽牌时如果 Draw 为空,GCS 会把 Discard 洗回 Draw,并发布 OnDrawPileShuffled,两个牌堆都为空时,本次抽牌结束
起手牌与每回合抽牌
战斗开始时,GCS 会从 Run Master Deck 或玩家的 Starting Deck 创建抽牌堆,完成洗牌后优先把 Innate 卡放入起手牌,再补足 BattleRules.StartingHandSize,从第二个玩家回合开始,运行时会清空 DrawnThisTurn,发布 OnDrawPhase,执行抽牌阶段行为,再按 BattleRules.DrawPerTurn 抽牌
第一回合不会重复这一流程,因为起手牌已经在初始化期间生成
手牌上限来自 BattleRules.MaxHandSize,并可能被 Hand Size Hook 修改,手牌已满时会根据操作来源采用不同结果:
-
常规起手牌与回合抽牌会把超出的牌送入弃牌堆,并为每张溢出牌发布
OnCardDiscarded、执行状态与卡牌的 On Discard Behavior -
GCSApi.DrawCards与 FlowGraph 的Draw Cards会在手牌达到上限时停止继续抽牌;每张实际抽到的牌都会发布OnCardDrawn,按快照顺序完成玩家状态的 On Drawn 分支,再完成该卡自己的分支,之后才处理下一张牌 -
向已满手牌添加或移动卡牌时,目标卡会进入弃牌堆并执行完整 On Discard 生命周期,新增卡牌的返回结果仍表示成功创建的实例
打出卡牌
GCSApi.TryPlayCard(card, target) 成功后,运行时按以下顺序处理卡牌:
- 确认卡牌仍在 Hand,解析并验证目标,再检查阶段、费用与出牌 Hook
- 先把实例移出 Hand,再扣除能量、在有效费用为正时发布
OnEnergySpent并将实例加入PlayedThisTurn - 发布
OnCardPlayed,依次执行卡牌与状态的 On Play Behavior - 注册卡牌产生的持续 Hook
- On Play 已明确移动实例时保留该位置,否则带
Exhaust的卡牌通过事件与状态、卡牌生命周期进入 Exhaust,其余卡牌进入 Discard,但默认的出牌后弃牌位置不算弃牌生命周期
On Play 开始前卡牌已经不在实时手牌中,因此 Behavior 内的手牌数量、抽牌与 Choice 都读取出牌后的 Hand;运行时会保证实例最终只属于一个战斗牌堆,自定义 UI 只需响应卡牌事件并重新读取牌堆,不要在点击卡牌时手动删除实例
回合结束时处理手牌
默认的 HandDiscardRule.DiscardOnTurnEnd 按以下规则处理剩余手牌
| 卡牌状态 | 结果 |
|---|---|
| 普通手牌 | On Turn End Behavior 没有明确移动时,通过弃牌事件与状态、卡牌生命周期移入 Discard |
Retain | On Turn End Behavior 没有明确移动时留在 Hand,并发布 OnCardRetained |
Ethereal | On Turn End Behavior 没有明确移动时,通过消耗事件与状态、卡牌生命周期移入 Exhaust |
Encounter 使用 HandDiscardRule.Persist 时,普通手牌也会保留,但 Ethereal 仍然进入 Exhaust;On Turn End Behavior 已经把实例移入其他牌堆时,默认清理会保留该结果并移除其他牌堆中的重复引用
创建和移动卡牌
| 任务 | API |
|---|---|
| 抽牌 | GCSApi.DrawCards(count) |
| 向手牌添加卡牌 | GCSApi.AddCardToHand(card) |
| 移动已有实例 | GCSApi.MoveCardToPile(instance, pile) |
| 洗牌 | GCSApi.ShuffleDrawPile() |
PileScope.Master 和 PileScope.All 作为读取范围与移动目标时含义不同
| 值 | 读取 | 作为移动目标 |
|---|---|---|
Master | 将 Run Master Deck 转成临时实例用于查询 | 从四个战斗牌堆移除,不修改 Master Deck |
All | 合并四个战斗牌堆,不包含 Master Deck | 从四个战斗牌堆移除 |
把现有实例显式移入 Discard 或 Exhaust 时,运行时会发布 OnCardDiscarded 或 OnCardExhausted,并在当前效果波次继续前完成对应的状态与卡牌 Behavior,再次把同一实例移到当前牌堆属于无操作,不会重复发布事件或执行生命周期
使用 Add Cards 直接在 Discard 或 Exhaust 创建实例只会建立初始位置,不算弃牌或消耗生命周期;普通卡牌与嵌套卡牌在出牌结束后进入默认 Discard 位置时同样不会发布 OnCardDiscarded,显式移动、满手溢出与回合结束清理才会执行弃牌生命周期
不要直接修改 CardPile.Cards,直接写入列表会绕过牌堆规则、卡牌事件和 UI 刷新
Run Master Deck
Run Master Deck 在同一局 Run 的多场战斗之间保留,记录卡牌 GUID、升级状态和费用变化,开始新战斗时,运行时根据它创建新的 CardInstance 和战斗牌堆
| 任务 | API |
|---|---|
| 读取或替换 | GetMasterDeck()、SetMasterDeck() |
| 添加或移除卡牌 | AddCardToMasterDeck()、RemoveCardFromMasterDeck() |
| 升级卡牌 | UpgradeCardInMasterDeck() |
| 开始新 Run 前清空 | ClearMasterDeck() |
修改 Master Deck 不会追溯改变已经开始的战斗,多场 Run 的初始化方式见战斗启动
用事件刷新卡牌 UI
卡牌被抽取、打出、弃置、消耗、保留、加入手牌、变形、升级或发生其他修改时,GCS 会发布对应事件,事件适合决定何时播放动画或刷新界面,当前牌堆内容仍应重新读取 GCSApi.Hand() 等查询方法