1. 项目概述从零构建一个卡牌构筑游戏引擎最近在GitHub上看到一个挺有意思的项目叫guladam/deck_builder_tutorial。光看名字很多开发者尤其是对游戏开发感兴趣的朋友可能立刻就能会心一笑。没错这正是一个关于如何从零开始实现一个“卡牌构筑”类游戏核心机制的教程项目。这类游戏比如《杀戮尖塔》、《怪物火车》等以其深度的策略性和“每一局都是新体验”的Roguelike魅力风靡全球。但你是否想过抛开华丽的画面和复杂的剧情它的核心玩法——也就是那个让你不断获取新卡牌、优化卡组、挑战敌人的循环——是如何被构建出来的这个项目就是一把打开这扇门的钥匙。它不是教你复刻某个具体的游戏而是聚焦于构建一个通用的、可扩展的卡牌构筑游戏引擎。这意味着你学到的不是某个游戏的“皮肤”而是其内在的“骨架”和“肌肉”。无论你是想制作一个奇幻背景的冒险游戏还是一个科幻题材的策略游戏甚至是完全不同的题材只要核心玩法是卡牌构筑这个教程提供的思路和代码都能成为你坚实的起点。对于初学者它能帮你理清这类游戏最核心的数据结构卡牌、卡组、玩家状态和游戏循环抽牌、出牌、结算效果。对于有一定经验的开发者它能提供一套经过思考的架构设计比如如何优雅地处理卡牌效果这种高度动态和复杂的行为如何设计一个灵活的事件系统来驱动游戏逻辑这些都是实际项目中会遇到的硬骨头。接下来我们就深入这个“教程”的内部看看一个卡牌构筑游戏是如何被一步步搭建起来的。2. 核心架构与设计思路拆解构建一个卡牌游戏引擎最忌讳的就是一开始就埋头写代码想到哪写到哪。deck_builder_tutorial项目或者说一个合格的卡牌构筑引擎的成功首先依赖于一个清晰、松耦合的架构设计。这就像盖房子先有蓝图才能保证结构稳固后续装修添加新卡牌、新机制也方便。2.1 核心数据模型一切的基础卡牌游戏的核心是数据。我们需要用代码精确地定义游戏中的各种“实体”。卡牌 (Card) 模型这是最基本的单位。一张卡牌不仅仅是贴在屏幕上的图片它背后是一系列属性的集合。一个典型的Card类可能包含以下字段id: 唯一标识符用于在数据库或配置文件中精确查找。name: 卡牌名称如“火球术”、“格挡”。cost: 使用这张卡牌需要消耗的资源通常是法力值、能量等。type: 卡牌类型例如“攻击”、“技能”、“能力”、“诅咒”。类型决定了卡牌的基本行为规则比如“能力”牌打出后永久生效不进入弃牌堆。description: 卡牌的文本描述用于向玩家说明效果。effects:这是最核心的部分。它是一个效果列表每个效果都是一个独立的行为单元。例如一个效果可能是“对目标造成5点伤害”另一个可能是“获得5点格挡”。将效果从卡牌主体中分离出来是实现灵活性的关键。卡组 (Deck) 与区域 (Zone) 模型玩家拥有的卡牌不是散乱一堆的它们被组织在不同的“区域”中共同构成了玩家的“牌库系统”。抽牌堆 (Draw Pile)游戏开始时玩家所有卡牌洗匀后放置的地方。回合开始时从这里抽牌。手牌 (Hand)当前回合玩家可以使用的卡牌。通常有上限比如10张。弃牌堆 (Discard Pile)本回合使用过或弃置的卡牌去处。消耗牌堆 (Exhaust Pile)某些卡牌使用后会被“消耗”移出本局游戏。这是一个独立区域。卡组 (Deck)作为一个整体管理类它并不直接持有所有卡牌而是持有上述各个区域的引用并提供洗牌、抽牌、移动卡牌 between zones 等方法。这种“区域化”管理是卡牌游戏逻辑清晰的基石。玩家 (Player) 与敌人 (Enemy) 模型他们不仅是拥有卡组的实体更是拥有状态的生命体。health: 生命值。maxHealth: 生命上限。block: 格挡值用于抵消即将受到的伤害。energy: 当前能量/法力值每回合刷新用于打出卡牌。powers(或Buffs/Debuffs)这是一个状态列表存放着持续生效的效果比如“每回合开始时获得1点力量”、“脆弱受到的伤害增加50%”。将状态设计为可叠加、有时效的对象是处理复杂互动的前提。2.2 事件驱动架构游戏的神经系统卡牌游戏是一个充满互动的系统打出卡牌触发效果效果影响玩家或敌人状态状态改变可能又触发其他效果。如果用一堆if-else硬编码这些互动代码很快就会变成无法维护的“面条代码”。事件系统 (Event System)是解决这一问题的银弹。其核心思想是游戏中的任何重要动作如“抽牌前”、“受到伤害时”、“回合结束时”都作为一个“事件”被广播出去。任何对此感兴趣的对象如卡牌效果、玩家状态都可以“监听”这些事件并在事件发生时执行自己的逻辑。例如玩家打出“燃烧打击”造成7点伤害如果敌人有“易伤”则额外造成3点伤害。卡牌效果系统解析出“造成伤害”效果并创建一个DamageEvent事件包含伤害源、目标、基础伤害值等信息。在应用伤害前系统广播DamageEvent。“易伤”状态监听到了这个事件检查目标是否是自己施加的对象。如果是它修改事件中的伤害值最终伤害 基础伤害 * 1.5。事件处理完毕系统根据最终伤害值扣减目标生命值。同时“对敌人造成伤害时抽一张牌”的卡牌效果也监听了此事件并触发抽牌。这种方式的好处是解耦。卡牌“燃烧打击”不需要知道“易伤”状态的存在它只负责发布一个“造成伤害”的事件。“易伤”状态也不需要知道是谁造成了伤害它只关心“伤害事件”本身。添加新卡牌或新状态时你只需要让它们监听或发布相应的事件而无需修改大量现有代码。2.3 效果解析系统卡牌的灵魂卡牌的文字描述是给人看的计算机需要的是可执行的逻辑。因此我们需要一个效果解析系统将诸如“对随机敌人造成3点伤害3次”或“获得等同于你手牌数量的格挡”这样的自然语言描述转化为一系列具体的、可执行的操作指令。在教程项目中一种常见的实现方式是使用可序列化的效果数据对象。每张卡牌的配置可能是JSON或ScriptableObject中effects字段不是一个字符串而是一个结构化的数据列表。{ “card_id”: “fireball”, “effects”: [ { “type”: “Damage”, “target”: “SelectedEnemy”, “value”: 8 } ] }在代码中有一个EffectResolver或类似的系统它根据type字段创建对应的效果处理器如DamageEffect、BlockEffect、DrawCardEffect。每个处理器都知道如何响应游戏事件来执行自己的逻辑。对于更复杂的效果如“随机”或“基于条件”效果数据中会包含额外的参数如count: 3, random: true。这种设计使得数据驱动成为可能。策划或开发者可以通过修改JSON配置文件来调整卡牌效果、创建新卡牌而无需重新编译代码极大地提升了开发迭代速度。3. 核心模块实现详解理解了顶层设计我们就可以深入到几个最关键模块的实现细节中。这里会包含大量“踩坑”后总结的经验也是教程项目的精华所在。3.1 卡牌与卡组管理系统的实现卡牌实例化与内存管理这里有一个重要的概念区分卡牌模板 (Card Template)和卡牌实例 (Card Instance)。模板是只读的数据定义来自JSON而实例是游戏运行时存在于某个玩家卡组中的具体对象。为什么需要区分因为同一张卡牌如“打击”在玩家的卡组里可能有10张它们共享同一个模板数据但每张牌都是独立的实例。实例上可以持有一些临时状态比如本场游戏中被“强化”后增加的伤害值。在实现时我们通常用模板数据初始化一个实例后续对这张牌的所有修改都发生在实例上。抽牌算法的“陷阱”抽牌听起来简单从抽牌堆顶部拿一张牌到手牌。但这里有几个细节抽牌堆为空时标准规则是将弃牌堆的所有牌洗入抽牌堆然后继续抽。这个“洗牌”操作是一个关键的游戏节点。在代码中你需要确保在移动卡牌时正确清空弃牌堆引用避免内存泄漏或逻辑错误。抽牌数量效果可能是“抽2张牌”。你需要循环抽牌并且每次循环后都要检查抽牌堆是否为空是否需要洗牌。抽牌事件在抽每张牌的前后都应该触发相应的事件如OnDrawCard、OnCardDrawn以供其他效果如“抽到时触发”进行响应。实操心得我强烈建议将“洗牌”作为一个独立且严谨的方法来实现。它不仅是将一个列表的卡牌转移到另一个列表还应该广播一个OnDeckShuffled事件。很多卡牌效果如“洗牌时将一张‘伤口’加入抽牌堆”都依赖于这个事件。同时在调试时为每个区域的卡牌数量打印日志能快速定位抽牌逻辑错误。卡牌区域的移动移动卡牌是最高频的操作。一个健壮的方法MoveCard(CardInstance card, Zone from, Zone to)是必不可少的。它需要处理从原区域的列表中移除该卡牌实例。向目标区域的列表中添加该实例。更新卡牌实例内部对当前所属区域的引用。触发相应的事件如OnCardMovedToHand。3.2 灵活可扩展的效果系统构建效果系统是引擎的“魔法部门”它的设计直接决定了你能做出多酷的卡牌。效果工厂模式这是实现数据驱动效果的关键。我们定义一个IEffect接口它可能包含一个Execute(GameContext context)方法。然后我们有一个EffectFactory类它根据效果数据中的type字符串如“Damage”来创建对应的DamageEffect对象。public interface IEffect { void Execute(GameContext context); } public class EffectFactory { private Dictionarystring, FuncEffectData, IEffect _creators; public IEffect CreateEffect(EffectData data) { if (_creators.TryGetValue(data.type, out var creator)) { return creator(data); } throw new ArgumentException($Unknown effect type: {data.type}); } }复杂效果组合一张卡牌往往有多个效果。例如“造成5点伤害然后如果目标生命值低于50%再造成5点伤害”。这可以通过两种方式实现顺序执行卡牌的effects列表按顺序执行。简单直观适合大多数情况。效果链与条件判断设计一种ConditionalEffect它内部包含一个条件判断器和一个子效果。只有条件满足时才执行子效果。这样就能实现上述的“如果...则...”逻辑。将条件也设计成可配置的数据如{“type”: “TargetHealthPercentageBelow”, “threshold”: 0.5}系统的灵活性会再上一个台阶。目标选择机制效果需要对谁生效是玩家自己当前选中的敌人所有敌人随机敌人这需要一套目标选择 (Target Selection)系统。在效果数据中可以包含一个target字段。游戏逻辑在执行效果前会根据这个字段解析出目标列表。例如当玩家从手牌中选择一张以“单个敌人”为目标的卡牌时UI会要求玩家点击一个敌人这个被点击的敌人就会作为目标传入效果执行逻辑。3.3 状态Buff/Debuff系统的设计状态系统处理那些持续多回合的效果如“中毒”每回合开始时受到伤害、“力量”增加造成的伤害。状态作为独立实体不要将状态作为玩家属性的一堆布尔值和数值来管理。应该创建一个Power或Status类包含名称、描述、图标、剩余回合数以及最重要的——行为钩子 (Hooks)。这些钩子就是状态监听的事件。一个“脆弱”状态会监听OnDamageCalculated事件将伤害乘以1.5。一个“中毒”状态会监听OnTurnStart事件对宿主造成伤害并减少层数。一个“金属化”状态会监听OnTurnEnd事件获得格挡。状态的堆叠与刷新同类状态是叠加层数如中毒3还是刷新持续时间这需要在状态定义时明确。通常Debuff负面状态叠加层数Buff正面状态刷新持续时间。在代码中当尝试添加一个已有状态时你需要检查策略并执行相应操作是增加stack变量还是重置duration变量。避坑指南状态移除的时机非常重要。必须在状态效果应用之后再减少其持续时间或层数。例如“中毒”在回合开始时造成伤害然后层数减1。如果顺序反了最后一层中毒层数为1将永远不会造成伤害因为你在造成伤害前就已经把它移除了。同样监听OnTurnEnd事件的状态要小心处理回合结束时的连锁反应。4. 游戏循环与战斗流程实现有了静态的数据和模块我们需要一个动态的“发动机”来驱动一切运转这就是游戏主循环。对于一个回合制卡牌构筑游戏其核心循环非常经典。4.1 回合制状态机游戏应该处于一个明确的状态中并且只在特定状态下允许特定操作。这通常用一个状态机 (State Machine)来实现。玩家回合开始 (PlayerTurnStart)触发OnPlayerTurnStart事件。刷新玩家的能量例如重置为3点。抽牌通常抽5张。某些状态效果在此阶段结算如“在你的回合开始时抽1张牌”。状态机切换到PlayerTurnMain。玩家回合主阶段 (PlayerTurnMain)这是玩家可以自由操作的阶段。游戏等待玩家输入。玩家可以打出手牌消耗能量触发效果、使用药水、查看信息、结束回合。每次打牌后需要检查手牌是否已空或能量是否不足以打出任何牌但这通常不强制结束回合。玩家回合结束 (PlayerTurnEnd)当玩家点击“结束回合”按钮时触发。触发OnPlayerTurnEnd事件。结算玩家回合结束时触发的状态效果如“回合结束时失去所有格挡”。清空玩家本回合的格挡值如果规则如此。将手牌全部移入弃牌堆。状态机切换到EnemyTurnStart。敌人回合 (EnemyTurn)敌人根据其预设的AI模式行动攻击、防御、施加状态等。所有敌人行动完毕后触发OnEnemyTurnEnd事件。状态机切回PlayerTurnStart开始新回合。使用状态机可以清晰地划分逻辑避免玩家在敌人回合出牌之类的bug。每个状态转换都是触发事件的好时机。4.2 敌人AI与行动模式敌人的AI不需要像RTS游戏那样复杂。对于卡牌游戏敌人的行为通常是预先设计好的“模式”或“意图”序列。意图系统 (Intent System)这是《杀戮尖塔》开创的优秀设计。在每个敌人回合开始前或在玩家回合结束时就提前显示敌人下回合要做什么如“攻击造成12点伤害”、“防御获得10点格挡”、“蓄力下回合攻击力翻倍”。这极大地增加了游戏的策略性和透明度。实现上每个敌人都有一套行动模式Pattern可能是一个简单的列表也可能是一个带权重的随机选择。在EnemyTurnStart阶段为每个敌人计算其下一个“意图”并显示在UI上。在EnemyTurn阶段敌人简单地执行其预设的意图。简单的AI模式示例攻击模式如果玩家没有格挡或格挡较低高概率选择攻击意图。防御模式如果自身生命值较低高概率选择防御获得格挡意图。特殊技能每过N回合释放一次强大的特殊技能。经验分享敌人的AI数据最好也是可配置的如JSON。你可以为每个敌人定义一个行动列表[ {“intent”: “ATTACK”, “value”: 8, “weight”: 70}, {“intent”: “BLOCK”, “value”: 6, “weight”: 30} ]。weight是权重用于随机选择。这样调整敌人行为就变成了修改配置文件无需改动代码。4.3 胜利、失败与奖励循环战斗是卡牌构筑游戏的核心循环之一而战斗的结果决定了游戏的进程。战斗结算胜利条件所有敌人生命值 0。失败条件玩家生命值 0。结算时需要仔细处理各种效果。例如是否有“死亡时触发”的效果是否需要在战斗结束瞬间移除所有临时状态确保在进入奖励界面或地图界面前将战斗场景干净地重置。奖励系统这是驱动玩家进行“下一局”的关键动力。通常包括卡牌奖励从3张随机卡牌中选择1张加入你的牌组。这里需要一套过滤和权重系统避免在游戏初期就出现过于超模的卡牌。遗物/道具奖励提供被动增益的强大物品。其实现方式与状态系统类似但通常是永久生效或触发式效果。生命恢复/金币奖励。删牌/升级卡牌允许玩家从牌组中删除一张牌或将一张牌升级为更强版本升级本质上是切换到另一张配置更强的“卡牌模板”。实现奖励系统时关键是要有一个全局的游戏进度管理器 (Run Manager)。它记录玩家当前的生命值、金币、牌组、拥有的遗物、当前地图位置等。每次战斗胜利后由这个管理器来负责弹出奖励选择界面并根据玩家的选择更新进度状态。5. 数据驱动与配置化实践一个专业的游戏引擎必须将“数据”和“逻辑”分离。策划甚至是你自己应该能够通过修改文本文件或使用编辑器工具来调整游戏内容而无需程序员介入。5.1 使用JSON定义游戏内容JSON是存储游戏数据的绝佳格式它人类可读、机器可解析且几乎所有编程语言都支持。卡牌配置示例{ “cards”: [ { “id”: “strike”, “name”: “打击”, “type”: “ATTACK”, “cost”: 1, “rarity”: “BASIC”, “description”: “造成6点伤害。”, “effects”: [ { “type”: “DAMAGE”, “target”: “SELECTED_ENEMY”, “value”: 6 } ], “upgrade”: { “id”: “strike”, “description”: “造成9点伤害。”, “effects”: [ { “type”: “DAMAGE”, “target”: “SELECTED_ENEMY”, “value”: 9 } ] } }, { “id”: “defend”, “name”: “防御”, “type”: “SKILL”, “cost”: 1, “description”: “获得5点格挡。”, “effects”: [ { “type”: “BLOCK”, “target”: “SELF”, “value”: 5 } ] } ] }敌人配置示例{ “enemies”: [ { “id”: “cultist”, “name”: “邪教徒”, “health”: 48, “intents”: [ {“type”: “BUFF”, “id”: “ritual”, “weight”: 1}, {“type”: “ATTACK”, “value”: 6, “weight”: 2} ] } ] }在游戏启动时你的引擎需要加载这些JSON文件解析并构建出内存中的卡牌模板库、敌人模板库等。5.2 资源管理与加载策略当游戏内容增多会有大量的JSON文件、图片卡牌图、敌人立绘、音效等资源。你需要一个资源管理器来统一加载和缓存它们。异步加载特别是图片和音效必须使用异步加载避免游戏启动时或场景切换时的卡顿。引用与缓存卡牌配置中可能只保存图片的路径字符串如“art”: “images/cards/strike.png”。资源管理器根据这个路径去加载图片并缓存起来。当需要创建一张“打击”卡牌的UI时从缓存中获取图片资源。分包与按需加载对于大型游戏可以考虑将不同章节或类型的资源分成不同的AssetBundle在Unity中或类似的数据包只在需要时加载减少初始内存占用。5.3 扩展性思考Mod支持如果你的引擎设计良好支持玩家社区制作Mod模组将是水到渠成的事情。核心在于明确的加载路径引擎除了加载内置的Data/Cards.json还应扫描像Mods/MyCoolMod/Cards.json这样的目录。ID命名空间为了避免不同Mod的卡牌ID冲突可以引入命名空间如“my_mod:super_sword”。钩子与事件暴露将核心的事件系统如OnCardPlayed、OnGameStart暴露给Mod脚本允许Mod作者注入自定义逻辑。脚本支持对于极度复杂的效果纯JSON配置可能不够用。可以考虑集成一个轻量级脚本语言如Lua让效果字段可以指向一段脚本代码。这给了Mod作者最大的灵活性。6. 常见问题、调试技巧与性能优化在实际开发中你会遇到各种各样的问题。以下是一些典型问题及其解决思路以及如何让游戏运行得更顺畅。6.1 逻辑错误排查清单卡牌游戏的逻辑bug有时非常隐蔽因为它们可能只在特定的卡牌组合、状态叠加和时机下才会触发。问题现象可能原因排查方法卡牌效果未触发1. 效果数据未正确加载或解析。2. 效果类型字符串拼写错误工厂无法创建。3. 效果执行所需的目标Target为null。4. 效果逻辑中有未处理的异常导致中断。1. 打印或调试查看加载后的卡牌对象确认effects列表不为空且数据正确。2. 检查EffectFactory的注册字典确认类型字符串完全匹配注意大小写。3. 在执行效果前检查context.Target或context.Targets是否有效。4. 在效果执行代码块周围添加try-catch打印异常信息。状态效果叠加不正确1. 状态添加逻辑错误重复添加了对象而非叠加层数。2. 状态到期移除的逻辑有误可能在错误的事件中移除。3. 状态效果的计算时机不对比如在伤害结算后才应用增伤。1. 在添加状态的方法内先查找宿主是否已有同名状态。有则增加stack无则创建新实例。2. 仔细检查状态监听的事件如OnTurnEnd和减少duration/stack的代码顺序。3. 使用事件系统时确保状态监听的事件优先级正确在核心逻辑如基础伤害计算之后但最终应用之前执行。抽牌后游戏卡死或崩溃1. 抽牌堆、手牌、弃牌堆的引用管理混乱导致无限循环如抽牌触发洗牌洗牌又触发抽牌。2. 在遍历区域卡牌列表时修改了该列表并发修改异常。1. 在抽牌和洗牌方法中加入深度日志打印每次操作前后各区域的卡牌数量观察循环。2.绝对不要在foreach循环中直接对正在遍历的列表进行增删操作。如果需要先收集要操作的卡牌到临时列表循环结束后再处理。敌人AI不行动或行动异常1. 敌人意图计算逻辑有bug返回了无效的意图。2. 执行意图的代码未能正确处理所有意图类型。3. 敌人状态如眩晕未在AI计算时被考虑。1. 在EnemyTurnStart阶段调试输出每个敌人计算出的意图对象。2. 在执行意图的switch-case或if-else块中添加default分支并记录错误日志。3. 在计算可用意图前检查敌人是否拥有“眩晕”等禁止行动的状态。6.2 性能优化要点尽管卡牌游戏看起来不复杂但不当的实现仍可能导致性能问题尤其是在移动设备上。对象池管理卡牌UI战斗中最耗性能的操作之一是频繁创建和销毁卡牌UI对象用于手牌、抽牌堆等。使用对象池是必须的。在场景初始化时预先实例化一定数量的卡牌UI预制体并禁用它们。需要显示一张牌时从池中取一个可用的对象设置其数据图片、文字并启用当卡牌进入抽牌堆或弃牌堆不可见时将其UI对象禁用并放回池中。这能极大减少GC垃圾回收压力。避免每帧查找不要在Update()方法里频繁使用GameObject.Find()或GetComponent()来查找对象。在Start()或Awake()中缓存这些引用。例如将GameManager、UIManager等核心组件设为单例或通过依赖注入获取并保存到静态变量或字段中。效果计算的缓存有些效果计算可能比较昂贵比如“造成等同于你手牌中攻击牌数量的伤害”。如果这个效果在一回合内可能被多次查询例如多个效果同时监听计算可以考虑在回合开始或手牌变化时计算一次并缓存结果直到相关条件改变时再更新缓存。谨慎使用反射如果你用反射来根据字符串名创建效果或状态类要知道反射是有性能成本的。在游戏初始化时建立字符串到类型的映射字典之后通过字典查找来创建实例这比每次都反射要快得多。6.3 测试策略如何保证复杂互动的稳定性卡牌游戏是组合爆炸的典型手动测试所有卡牌组合是不可能的。你需要一些策略来保证质量。单元测试核心模块为Deck洗牌、抽牌、EffectResolver效果解析与执行、Status状态叠加与触发等核心类编写单元测试。模拟各种边界情况如空牌组抽牌、零伤害效果、状态层数溢出等。集成测试与场景测试创建一些固定的测试场景例如“玩家拥有A、B、C三张牌敌人有X状态检查打出组合技后的结果”。自动化运行这些场景断言最终的生命值、状态等是否符合预期。这能捕捉到模块间交互产生的bug。日志系统是你的朋友建立一个详细的、可开关的日志系统。记录关键事件[Game] Card Played: Fireball - Enemy Goblin[Event] DamageCalculated: 10 - 15 (Vulnerable)[State] Poison applied to Goblin, stack3。当出现诡异bug时查看完整的日志流水线能帮你快速定位问题发生的位置和顺序。设计模式命令模式 (Command Pattern) 用于回放与测试考虑将玩家的每一个操作出牌、结束回合都封装成一个“命令”对象。这不仅可以让游戏逻辑更清晰还能轻松实现“悔棋”功能。更重要的是对于测试你可以录制一系列命令然后像播放磁带一样回放整个战斗过程这对于复现和调试偶发bug至关重要。走到这里你已经拥有了一个功能完整、架构清晰的卡牌构筑游戏引擎核心。它可能还没有酷炫的动画和音效但所有重要的齿轮都已经就位并能协同工作。从理解数据模型到实现事件驱动架构从构建效果系统到设计游戏循环每一步都是在为这个数字卡牌世界奠定基石。记住最好的学习方式是动手。用这个引擎做出一张你自己设计的、效果疯狂的卡牌然后和它战斗看看会发生什么。那种创造和掌控的感觉正是游戏开发最原始的乐趣所在。