资讯动态

从零构建卡牌游戏框架:Unity多人CCG核心架构与实现解析

发布时间:2026/10/20 8:07:20 来源:尧图企业网站定制
玩卡牌游戏的人很多但能说清楚一张卡牌从数据库到屏幕、再从手牌到战场背后到底发生了哪些事的开发者其实不多。我早几年接手过一个类《炉石传说》的多人卡牌项目技术选型定在Unity当时市面上没有一个开箱即用、能直接支撑复杂卡牌效果多人同步的完整方案于是只能从零开始搭一套属于自己的卡牌游戏框架。这个框架后来被我反复打磨陆续支撑了好几个不同题材的CCG玩法项目也算是沉淀出了一些真正经得起实战考验的东西。这篇博文就把这套CCG Kit的核心技术架构与实现原理完整拆开来讲包括卡牌数据模型、区域系统、回合与优先级管理、Buff效果引擎、多人同步方案这几个关键模块。无论你是打算从零写一个卡牌Demo的Unity新手还是已经在做商业卡牌项目、正为效果组合和联机同步头疼的开发者这篇文章都会给你一份可以直接参考的落地思路。1. 卡牌数据模型把一张牌拆成三层从源头杜绝改一处崩三处很多刚开始写卡牌项目的人最喜欢干的事情就是给每张卡建一个类比如FireballCard : MonoBehaviour然后在里面写public int damage 6;。这个做法在只有十几张卡的时候很爽但当你开始做几百张卡、并且每张卡还有不同版本比如普通版、金卡版、衍生卡的时候你会发现代码里到处都是这张牌的伤害写死在这里、那张牌的效果又写死在那边改平衡性要翻遍整个项目。我踩过这个坑之后把数据模型彻底重构成了三层结构CardDefinition、CardInstance、CardActor。这套拆分的核心思想其实很简单把卡牌的模板数据、卡牌在游戏中某一局的具体状态、卡牌在场景中的可视与交互表现三者彻底解耦。1.1 CardDefinition不沾运行时逻辑的静态模板CardDefinition在Unity里我用ScriptableObject来实现它保存的是这张卡牌的元属性——卡牌名称、卡面图片引用、费用、攻击力、生命值、类型标签随从/法术/武器、以及一个CardEffectConfig列表。这里最关键的一个设计决策是CardDefinition里不允许存放任何MonoBehaviour组件引用也不允许直接操作场景对象。它就是一个纯数据的配置资产只负责回答这张卡是什么。这个设计的好处有两个。第一策划调平衡时只需要改ScriptableObject资产甚至可以在编辑器里写一个批量改数值的工具脚本不用动任何C#代码第二CardDefinition可以被多个CardInstance共享比如你有500张基础牌库但某局游戏里玩家A和玩家B各自复制了这500张卡牌的实例内存里只需要保留一份500个CardDefinition的资产引用而不用为每个玩家加载两遍牌库数据。我实测下来在移动端上这个优化非常重要——直接用实例复制会造成明显的内存翻倍而引用共享几乎没有额外开销。1.2 CardInstance掌管这一局的运行时快照CardInstance是一个纯粹的C#类不是MonoBehaviour它保存的是某张卡牌在当前对局中的动态数据当前费用、当前攻击力可能被Buff修改过、当前生命值可能受过伤、所在区域ID、持有者ID、以及一堆标记状态比如本回合是否被使用过是否已被沉默。为什么要单独拆出这一层因为卡牌在战斗中是被不断修改的——某张随从可能从-1费Boss手里拿到了Buff攻击力从5变成了9但它的CardDefinition里的基础攻击仍然得保持5。如果让运行时修改直接写进模板数据就会出现打完一局后牌库里所有同名卡都被永久Buff了这种灾难性Bug。我这里还做了一个很实用的约定CardInstance里保存的修改字段都带上基础值和当前值两个版本。比如攻击力有baseAttack和currentAttack生命值有baseHealth和currentHealth。任何Buff、光环、条件修正都只作用于current*字段而沉默、消除Buff等操作需要知道基础值才能正确还原。这个设计在做重置为初始状态类效果比如萨满的妖术、牧师的身心净化时省了无数头疼事。1.3 CardActor场景里的门面只做表现与输入CardActor才是挂在场景预制体上的MonoBehaviour组件负责卡牌在战斗场景中的表现和交互播放入场动画、承接拖拽指令、显示攻击力/生命值UI、播放卡牌使用时的特效等。CardActor自己不保存任何逻辑数据它只通过一个CardInstance的只读引用来获取展示数据同时把自己的交互事件被点击、被拖拽、被释放转发给更高层的控制器。这个分层让表现层可以随意替换——比如你想把卡牌展示从3D模型换成2D图片或者加一套全新的入场动画逻辑层完全不用动。实际开发中我最喜欢的用法是在攻击力变化时让CardActor的UI文本刷新。因为CardInstance实现了简单的属性变更通知OnStatChanged事件CardActor只需要订阅这个事件然后更新UI即可。效果引擎后面会讲到改了卡牌数据界面自动就跟着变了不会出现逻辑上Buff已经加了、画面上数值纹丝不动的脱节问题。2. 区域系统手牌、牌库、战场、墓地的数据流转与生命周期管控卡牌游戏里所有的行为归根结底其实只干了一件事把卡牌从一个区域移动到另一个区域。抽牌是把牌从牌库移到手牌打出随从是从手牌移到战场法术生效后进墓地是从手牌移动到墓地。区域系统如果设计不好你会发现在处理卡牌移动时到处写if判断——如果它原来在手牌要从列表里移除如果它原来在战场要处理死亡结算如果它进入墓地还要触发亡语……这种代码写多了以后任何一张新卡都可能因为漏了一个分支而出Bug。2.1 Zone枚举与ICardContainer接口我定义了一套基础区域枚举Deck牌库、Hand手牌、Battlefield战场、Graveyard墓地、Exile放逐区、SecretZone奥秘区、WeaponZone武器区等。每个区域对应一个ICardContainer实现。ICardContainer接口的核心方法非常简洁public interface ICardContainer { Zone ZoneType { get; } IReadOnlyListCardInstance Cards { get; } void AddCard(CardInstance card, int index -1); void RemoveCard(CardInstance card); }所有容器都实现同样的接口而真正的区域行为差异比如牌库需要洗牌、手牌需要排序、战场需要站位通过具体实现类来区分。这样上层逻辑在移动卡牌时只需要面向接口调用不需要感知它到底来自哪个区域。我见过的一些简化方案直接用ListCardInstance裸奔结果就是洗牌逻辑、区检索逻辑、事件触发逻辑满天飞强烈不建议。2.2 统一的MoveCard管道谁动了哪张牌大家全都知道有了区域容器之后下一步就是做一个统一的CardMover.MoveCard(sourceZone, targetZone, card, targetIndex)方法。这个方法是整个区域系统的咽喉要道触发BeforeCardMoved事件供沉默、冰冻、束缚等拦截类效果使用。从源容器移除卡牌。修改卡牌的CurrentZone字段。在目标容器添加卡牌可选索引。触发CardMoved事件所有关心这个事件的系统入场效果、光环触发、UI刷新都在这里响应。把移动逻辑收敛到一个管道里最大的价值在于任何卡牌只要进入一个新区域该区域特有的入场效果不需要在每张卡的效果脚本里分别重复处理。比如战吼效果统一挂在CardMoved事件里检查如果这张牌进入了战场并且是第一次从手牌打出就执行战吼。这样当你以后做一张从牌库直接拉出随从到战场的召唤牌时只要走同一个MoveCard管道战吼会被自动触发不需要为这种特殊召唤场景额外写一套触发逻辑。2.3 从堆叠到堆叠状态清理与失效引用区域系统还有一个容易踩的坑——失效引用。常见场景是这样的某张牌在战场上被Buff了Buff对象内部引用着这张牌实例然后这张牌被回手、被沉默、被变形Buff却还挂在原实例上。原实例如果进了牌库/墓地Buff仍然存活就会造成这张牌回手后攻击力居然还带Buff或者更诡异的Bug。我的解决方案是引入一个ICardStateCleaner清理器在所有MoveCard操作完成后会扫描这张卡身上的所有临时状态并依据状态允许存在的区域Tag来决定是保留、移除还是仅标记失效。比如魅惑Buff只允许存在于战场牌一旦回手Buff自动清除而本回合内下一张法术牌费用-X这类状态允许存在于手牌则不清除。这套规则的维护成本不高但为之后几百张卡的效果设计兜了底。3. 回合与优先级状态机驱动下的多人节奏控制任何一款有先后手之分的卡牌游戏核心玩法的节奏都建立在回合系统之上。普通的回合制可能只需要一个简单的switch语句但当你加入大量在回合结束时触发、敌方回合开始时抽牌、“当X事件发生时响应”的机制后就必须认真设计一个回合状态机。3.1 回合阶段的粒度别把所有逻辑塞进开始/结束两个钩子我会把每个玩家的回合拆成更细的阶段TurnStart、DrawStep、MainPhase、CombatStep、EndStep。实际上CCG Kit用的状态机远远不止这五个还包括ResponseWindow响应窗口、Mulligan换牌、InterruptStep插队结算等特殊阶段。每个阶段是一个独立的类实现ITurnPhase接口接口包含Enter()、Update()、Exit()三个方法。细粒度阶段的好处很直接我们需要一个精确的当X阶段开始时全局事件来挂钩各种效果。比如每当你回合结束时抽一张牌这个效果如果阶段只有开始/结束两个就只能靠区分谁的回合结束来猜语境而有了PlayerTurnEnd这个独立阶段效果系统可以直接订阅TurnEvents.OnPlayerTurnEnd(playerId)不需要自己判断。这个看似简单的设计实际让卡牌效果代码减少了很多重复的条件分支读起来也更像卡牌的原始描述文本。3.2 响应链与堆叠后发先至不能靠递归多人卡牌游戏里最复杂的一套逻辑是响应链。当玩家A打出一张法术玩家B如果手上有反制法术他可以选择响应A如果也有第二张反制还能继续跟。这个层层嵌套的过程和炉石传说的机密反制有异曲同工之处。很多人第一次实现响应链时喜欢用递归调用——CastSpell里面嵌套TryCounterTryCounter里再嵌套CastSpell——写起来爽但跑起来很快就会把调用栈撑爆而且出问题极难排查。我这边采用的是一个显式的响应堆栈Stack数据结构。每次有人打出一张牌CardEffectResolver就会往堆栈顶端压入一个ResolvedEffectNode然后询问当前回合玩家和对方玩家是否有意愿响应。只要有玩家响应就压入新的节点当没有人再响应时从堆栈顶部依次弹出并结算每个节点。这个入栈-询问-出栈的循环是整个效果结算系统的核心骨架。它天然支持后发先至的规则也和玩家在UI上的直观感受一致——最后打出的牌最先生效。3.3 回合控制权的切换与谁当前能操作的判定多人对战里现在该谁行动这件事如果只靠一个布尔量isMyTurn来维护那你就等着被异步效果打崩。因为你还要考虑响应窗口虽然有优先权但非回合玩家也能出牌、连击/打断效果、以及双方同时有操作意愿时的仲裁。在CCG Kit里回合系统维护了一个PrioritySeat当前持优先权的座位和一个TurnActionLock。所有允许操作的判断都统一走这两个对象的查询不再散落各处。举个例子当玩家A处于主阶段他打出随从后优先权会被交给对手B。对手B如果没有任何响应并点击结束优先权交回AA继续操作。如果B在响应链中打出一张法术优先权又被重新计算。整个过程中TurnActionLock负责确保只有持优先权的人才能把卡牌拖出边界、只有持优先权的人才能点击结束回合按钮。这套机制配合响应堆栈就能非常严谨地处理多人卡牌中复杂的回合穿插。4. 全局随机数与洗牌多人对战必须严守的同一随机数原则随机数在卡牌游戏里无处不在洗牌、随机伤害、随机目标、发现机制……如果你做单人游戏Random.Range随便用但一旦上了多人随机数的处理就变成了一个安全与公平性的硬骨头。很多项目在这里翻车因为两端各自随机会导致双方看到的战局不一致。4.1 统一随机源客户端不许自己造随机我在CCG Kit里定下了一条死规矩所有影响游戏结果的随机行为都必须经过一个全局GameRandom服务而GameRandom的最底层随机种子的生成与消费只允许发生在权威逻辑层服务端或承担权威职责的主机端。客户端本地如果想做抽卡包开包这类表演随机可以单独用一个与对局无关的随机源但是绝对不能让它影响到对局内战斗结果的生成。具体实现上我采用的是随机事件序列方案。权威端一次性生成一段随机数序列比如用种子递增索引的方式产生一万个随机数然后在不同随机操作中依次取用并把操作类型和索引广播到所有客户端。客户端收到随机事件后用同一个种子和自己的索引本地就能复算出完全相同的随机结果。这个方案的优点是短数据包就能同步随机结果而且排除了客户端上报随机结果导致作弊的可能性。4.2 洗牌算法与防窥牌洗牌算法本身是卡牌项目里另一个容易被低估的模块。我用的是经过验证改进的Fisher-Yates洗牌。但更重要的问题不是算法本身而是什么时候洗牌。玩家不该知道牌库的顺序而且理论上某些效果比如查看牌库顶3张牌会在不完全洗牌的情况下暴露部分牌序。所以CCG Kit里维护了一个DeckModel它内部保存完整牌序列表并提供Shuffle()、Draw()、PeekTop(int count)、RemoveAt(int index)等受控操作。所有能访问牌库顺序的操作都必须在调用前经过授权检查——比如检索牌库类效果在结算结束后必须重新洗牌防止玩家通过UI的细微延迟推测牌序信息。4.3 断线重连时的随机状态恢复多人卡牌还有一个容易忽略的问题——断线重连。玩家A掉线后重连如果游戏已经把随机状态推进了20步新连入的客户端需要知道当前的随机数索引。如果只同步seed不同步index重连后的随机结果和别人的会不一致。所以每个GameRandom实例都带一个CurrentIndex字段并在状态快照中定期保存。断线重连时只需要下发seed currentIndex客户端就能无缝接上随机序列这个细节在实测中帮我们避免了很多重连后伤害结果对不上的尴尬Bug。5. Buff与效果引擎怎么让任意两张牌的效果可以自由组合不变成屎山卡牌游戏的长线生命力靠什么靠新卡牌带来新机制靠效果之间的组合产生无穷变化。但如果效果系统写得死板每做一张新卡都要往主逻辑里塞一条if那你这个项目离崩溃不远了。CCG Kit的效果系统是我花最多时间反复重构的部分也是一开始最容易写成一坨的地方。5.1 从“技能类”到效果节点把卡牌描述拆成语法树我在最初的版本里给每张卡写一个继承自CardEffectBase的这个类重写Execute()方法。做前50张卡的时候很顺利做到第80张的时候发现很多效果都是由更小的原子效果组合出来的——对随机一个敌方随从造成2点伤害本质上是随机选择目标EnemyMinion造成2点伤害Damage。如果每个组合都建一个新类那组合爆炸是迟早的事。后来我彻底转向了**效果节点树Effect Node Tree**方案。每张卡的效果配置不再是一个类实例而是一个树形结构。树的叶子节点是原子效果造成伤害、抽牌、召随从等中间节点是控制类效果顺序执行、随机分支、条件判断、对所有目标重复执行等。这套数据结构配合一个简单的解释器模式只要定义好30个左右的原子效果和10个左右的控制节点就能组合出市面上绝大多数的卡牌效果。public abstract class EffectNode { public abstract IEnumerator Execute(EffectContext ctx); } public class DamageNode : EffectNode { public int Amount; public TargetSelector Target; public override IEnumerator Execute(EffectContext ctx) { var targets Target.Resolve(ctx); foreach (var target in targets) { target.TakeDamage(Amount, ctx.SourceCard); } yield break; } } public class SequentialNode : EffectNode { public ListEffectNode Children; public override IEnumerator Execute(EffectContext ctx) { foreach (var child in Children) { yield return child.Execute(ctx); } } }这个设计让策划配置新卡几乎不需要写代码——在编辑器里把节点拖来拖去再填参数就行程序只需要在出现真正前所未有的机制时加一个新节点类型。5.2 Buff的叠加、刷新与优先级Buff系统单独说是因为这里坑最多。我先设定了基础规则每个Buff实例拥有BuffTag比如加攻击力和StackMode叠加/刷新/互斥叠加模式下相同标签的Buff可以叠加层数刷新模式下重复施加只重置持续时间互斥模式下新Buff直接替换旧Buff。这种规则不是我想当然定的而是从大量CCG玩法抽象出的通用约定。比较麻烦的是Buff的生效时机。我采用的是被动Modifier监听的方式Buff可以注册到特定事件点上比如当随从受到伤害时、当随从攻击力被计算时、当回合结束时。比如一个使相邻随从2攻击力的光环类Buff不是直接去改相邻随从的currentAttack而是把自己挂载到攻击力计算事件上由计算管线动态累加。这么做的一个直接好处是相邻随从移动离开光环范围后攻击力修正自动消失不需要任何手动清理。坏处是攻击力计算变得更频繁但因为卡牌游戏规模有限战场最多7个随从性能完全不是问题。5.3 效果链的日志与回放没有它你根本没法调试组合技效果引擎的复杂度上升到一定程度后整个流程会变得完全不可见。尤其是当玩家打出一张牌因为另一张牌的效果从牌库里拉出第三张牌然后第三张牌又触发亡语这种连锁时如果没有一套完整的操作日志系统排查Bug就像在大海里捞针。我在框架里集成了一个ActionLog日志系统每当有卡牌移动、效果施放、Buff增减、伤害结算时都会追加一条日志。每条日志包含触发者、对象卡牌、节点类型、参数快照与时间戳。调试时打开日志面板整个对局就像看剧本一样一帧一帧复盘。这个方法在后期做复杂组合技时帮了大忙——有一次一个穿针引线的合作技能怎么都触发不了正确次数日志一看就发现是节点顺序在语法树上被配置反了改个顺序就解决当时省了至少两天排查时间。6. 多人联机方案选型状态同步为主帧同步为辅以及断线重连设计多人卡牌游戏的联机有两个主流路线状态同步和帧同步。很多从动作游戏转过来的同学一听到卡牌也要同步逻辑就本能地往帧同步靠但你要真用帧同步去做卡牌你会发现它比动作游戏更别扭因为卡牌游戏充斥着大量的、离散的、带随机性和条件分支的状态变更帧同步最擅长的确定性模拟在这里收益很有限。6.1 为什么卡牌游戏选状态同步我做技术预研时做过一个对比实验同一个移动端卡牌Demo分别用状态同步和帧同步实现。帧同步的版本跑通基本逻辑没问题但每次新增一个卡牌效果都要非常小心地检查各个客户端的模拟结果是否一致尤其是当效果节点树的配置可能因为版本差异导致两端配置表不一致时分支一旦产生差异整个对局就静默不同步了。而状态同步则简单得多客户端把操作意图发到权威端权威端执行效果引擎并广播结果非权威客户端只做表现层回放。卡牌游戏天然适合状态同步还有一个原因它没有动作游戏那种60帧的输入预测需求。玩家一个回合的操作频率低、决策密集网络延迟一百毫秒完全可以通过等待确认和效果动画演出掩盖掉。所以我最终选型是权威状态同步表现层用补间动画做平滑。6.2 客户端只管意图权威端只管逻辑这个架构在我项目里的实践是客户端UI操作拖卡牌、选择目标、点击按钮产生一个PlayerIntent消息意图消息发给权威端。权威端收到后先做合法性校验费用够不够目标合法吗当前是否拥有行动权校验通过后调用效果引擎执行完整的逻辑流程生成一个GameEvent列表再把这个列表广播给所有客户端。客户端收到GameEvent列表后按顺序播放动画演出。这样设计的好处是逻辑层完全可测试——我可以写一个跑在服务器上的AI脚本让两个AI客户端对打上万局全程不开Unity客户端只通过单元断言比如6费回合结束时场上随从总血量是否正确来回归效果组合。这个自动对局回归测试为我节省了大量的人工测试时间严重推荐大家为卡牌逻辑层做同样的框架支持。6.3 断线重连快照 事件流增量我之前提过随机数索引的恢复但断线重连不能只解决随机数。整套方案分三层首先权威端定期生成一个全量状态快照包含所有区域里每张卡的完整数据、当前回合阶段、当前优先权、随机数索引和Buff列表第二层是快照之间的增量事件流包括刚才提到的GameEvent列表、效果日志和操作记录。断线重连时客户端先拉取最近一个快照然后从该快照的索引开始重放增量事件流直到赶上最新状态。这个方案的坑在于增量事件过多时重连会很慢所以我把快照生成改为关键节点触发比如每次大回合开始、每次大型连锁结算完毕而不是固定时间间隔。实测下来大部分对局的重连恢复都在两秒内完成玩家体验基本无感。6.4 掉线托管与观战探测我还做了一套掉线托管策略当某一端掉线后权威端不会立刻判负而是等待一个可配置的时间窗口。在窗口内掉线方的所有操作由托管AI自动执行比如当前手牌里费用最高的一张牌如果没有更强解直接打出并攻击敌方英雄。这个策略在真实对局里非常实用——很多掉线其实是网络瞬断几十秒内就恢复了托管能保证局面不至于被白白空过回合。窗口结束后仍未重连才判定该玩家负。另外托管AI的行为日志也必须写入ActionLog这样重连和观战看到的都是完整动向不会出现我掉线那段时间到底发生了什么的迷茫感。7. 性能优化与攻防演练从Demo到可上线还有多远很多人的卡牌Demo能跑起来但离可上线还有不小距离。这一章聊聊我在CCG Kit的性能优化和数据校验上踩过的坑以及为什么要反复做对抗性测试。7.1 卡牌数据序列化与内存优化Unity在移动端上处理成百上千张卡牌数据的序列化时如果方案选得不好会产生大量GC Alloc垃圾回收分配。我早期版本就用ListCardInstance直接存储手牌和牌库每局游戏创建大量对象频繁的增删导致GC频繁触发移动端帧率一度掉到20帧以下。后来改动集中在两个方向一是使用ObjectPool对象池来复用CardActor预制体和CardInstance对象尤其是随从死亡进墓地、抽牌上手这些高频操作不再反复Instantiate/Destroy二是把卡牌静态数据从ScriptableObject改为二进制ScriptableObject打包运行时用Addressable按需加载避免一次加载所有卡面纹理。优化之后同一台测试机上帧率从20帧提升到接近60帧稳定内存峰值降了约40%。7.2 断线重连与状态一致性压测上线前我做了两台测试机器之间的随机对局压测脚本会随机模拟掉线、重连、切后台、改系统时间等行为持续跑一周。这一周抓到了好几个隐蔽的状态不同步Bug最典型的一个是当玩家在响应链期间退出重连重连后PrioritySeat没有被正确恢复导致该玩家的回合卡死在等待输入状态。修复方法是把PrioritySeat也纳入全量快照字段而不是靠客户端重连后重新上报触发。这个Bug如果不在压测中抓到上线后必然会有真实玩家遇到我卡住了什么都点不了的致命体验。7.3 效果引擎的边界测试打死循环与意外组合效果引擎最怕两种问题一是无限循环比如当A受到伤害时给B回血B回血又让A受伤这样的连锁二是意外组合导致数值溢出。我在配置每个效果节点时会给节点加上执行次数上限和全局连锁长度上限。当一次连锁结算的节点数超过某个阈值比如200个权威端强制打断连锁并记录异常日志。这个保护机制让策划们可以放心大胆地做各种疯狂操作组合而不用怕把对局跑死。另外在数值层面每次伤害结算都会做int溢出检查虽然设计上单卡数值一般不会超过几百但各种百分比叠加、翻倍、再翻倍的Buff堆起来后溢出其实完全有可能发生这里提前兜底绝对是值得的。8. 工具链与工作流整套框架如何配合策划与美术的高效迭代技术架构到这一步整套CCG Kit已经不止是一个运行库而是一套支撑内容生产的工具链。我最后想聊一下框架里那些不那么起眼、但实际生产力提升最明显的部分。8.1 可视化效果编辑器让策划配置卡牌而不是等程序排期前面提到效果节点树但如果在编辑器里只能手写JSON或XML配置那策划的接受度会很低。所以我写了一个Unity编辑器扩展用GraphView把效果节点树可视化。每个节点一个方块拖拽连线表示执行顺序节点参数直接在Inspector里填。策划配置一张新卡的常见效果几分钟就能完成而程序只需要处理真正的新机制。这个编辑器还有一个本地模拟按钮可以在编辑器里模拟打出这张卡立刻看到效果不用进游戏跑图。工具上线后卡牌内容生产量翻了接近一倍而且程序侧几乎没有为新卡做额外开发的排期。8.2 自动对局回归测试把“手感”也纳入版本回归我之前提过服务端跑AI对局用于逻辑回归其实这套自动化不仅测逻辑也测性能和稳定性。我在CI流程里加入了每日一万局AI对战每局结束后检查关键健康度指标对局时间是否在合理区间、有没有出现异常日志、有没有触发紧急保护。如果一万局里有任何一局触发保护机制CI会标记失败并输出对应的ActionLog摘要。这个流程在每次效果节点库有改动时都会跑一遍确保新机制没有引入潜在的无限连锁或者状态异常。可以说如果没有这套回归我不敢在每次版本更新时仍然保持对所有卡牌组合都不会崩的信心。8.3 热更新与版本兼容卡牌配置必须能秒级修复线上游戏还会遇到一个现实问题——某张卡牌数值失衡或者有Bug你不可能等一周后发新客户端版本修。所以卡牌的整体配置包括效果节点树的序列化数据我全部采用可热更资源格式JSON或二进制配置放在远端客户端在进入游戏主页时检查版本号发现更新就后台下载。这里有一个细节效果节点树的新节点意味着需要客户端代码重新编译所以纯粹调数值和参数能热更新但程序新增了原子效果节点类型则必须发客户端版本。我一般把新节点类型的发布合并到例行版本中线上只做参数级和配置级热更避免出现配置引用了客户端不存在的节点类型的冷启动时间差问题。9. 多平台适配与网络架构的边界取舍Unity卡牌项目通常绕不开移动端和PC端甚至可能还有WebGL版本。我最后想补充的是多平台联机和网络架构上的一些边界取舍。9.1 局域网对战与自定义服务器不同规模的不同选择如果只是做LanParty玩法或者本地多人测试可以选择一个玩家作为房间主机主机同时承担客户端与权威逻辑的双重职责其余玩家通过直连主机上报输入。这个模式没有独立服务器成本适合中小规模比赛或者Demo验证。如果要运营大规模天梯匹配还是需要独立的房间服务器来承载权威逻辑客户端只负责展现和输入。前者的实现复杂度低很多但主机玩家的网络带宽压力和内存压力都比较重最多支撑到4-8人同时在线后者则需要投入服务器资源与更复杂的部署运维但扩展性远好于前者。我这里在框架层面做了一个抽象IGameAuthority接口。服务端模式由ServerAuthority实现主机模式由HostAuthority实现单人练习模式由OfflineAuthority实现。三者的接口完全一致因此换模式不需要动游戏逻辑层的任何代码。这个设计看起来很常规但实际经历过Demo阶段跑主机模式、上线阶段切独立服务器的项目都会明白——一开始就把权威逻辑与具体网络传输方式解耦后期切换网络架构就不用伤筋动骨。9.2 平台差异输入方式与UI响应不同端的主要差异集中在表现层和输入层。移动端拖拽卡牌的判定阈值较小误触概率较高所以卡牌出手判定不只检测拖拽终点是否越过边界还要检测拖拽速度、停顿时间、目标是否明确可合法选择。PC端鼠标精度高可以做更严格的边界判定。我的框架通过一个ITouchInputAdapter接口适配不同端的输入差异逻辑层只接收已经解析好的操作意图完全不感知输入来源是鼠标、触屏还是手柄。这个抽象让跨平台版本只需要新增一个适配器而不会把多端差异的if/else散落到卡牌逻辑的各个角落。9.3 网络协议选择可靠传输不缺消息缺的是顺序多人卡牌对网络消息的要求是高可靠 有顺序 低吞吐量而不是像FPS那样追求低延迟高频率。TCP天然可靠有序但队头阻塞在某些极端弱网情况下会让消息延迟叠加UDP快但需要自己做可靠性和顺序处理。我采用的方案是在UDP之上封装了一层带序号和ACK的轻量可靠消息层每个消息带有一个单调递增序号接收端保证按序号顺序上抛给逻辑层。这个方案在弱网比如高铁、电梯环境下的抗丢包体验明显优于纯TCP实现而开发成本也完全可控——只需要处理序号、重传和丢弃乱序包这三件事完全不需要像帧同步那样做密集同步。顺带一提完整的状态快照传输我仍然走了HTTP下载分包而实时交互则走上述消息层尽量避免一个快照大包卡住对局内所有小消息队列。10. 开源心态与实操建议框架做完以后我还会怎么继续做下去CCG Kit做到这个程度已经可以支撑一个完整的多人卡牌项目从立项走到上线了。它解决的核心问题其实只有两个第一是让卡牌效果组合的灵活度不受代码架构掣肘第二是让多人对局的逻辑一致性和网络体验足够可靠。当你把这两件事做到位后剩下的工作就是往框架里填充内容了——新卡牌、新机制、新皮肤、新玩法模式框架本身不应该成为阻碍。如果让我给正在规划自己卡牌框架的人几条发自内心的建议总结成以下几条数据、逻辑、表现这三层一定要从第一天就分开。后面任何一步优化都依赖于这层地基。临时凑合的数据结构一定会让你在500张卡之后付出十倍代价。全局唯二的咽喉是卡牌移动管道和效果结算堆栈所有跨模块操作都尽量收敛到这两个管道上出问题时有统一的排查入口。多人同步从开始就要考虑随机数、优先权、状态快照三件套等上线了再补数据迁移和兼容处理会让人想哭。效果引擎的自动化回归测试不是可选项是生死线没有它每做一张新卡都可能无意中摧毁某个旧组合而你甚至不知道。网络层尽量做成可替换模式从单人打到多人、从本地打到公网不要改一行游戏逻辑代码。这一点在多平台发行和多玩法运营时救命指数极高。如果你是从零开始写卡牌框架CCG Kit这套路子的每一步都可以先在Demo规模下先验证再抽象不用一口气搭完所有模块但架构方向和核心抽象的边界最好在动手前就想清楚。把这个框架沉淀下来之后无论是做修仙题材、三国题材、科幻题材还是做完全自创的玩法我都只需要在内容层堆量再针对题材特性加少量新节点类型就行了——这种框架稳定内容自由的感觉大概是卡牌游戏程序最舒服的终局状态了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑