Godot 4开发Roguelike卡牌游戏:从架构到实战
1. 项目概述为什么选择Godot 4来构建你的卡牌构筑Roguelike如果你对《杀戮尖塔》、《怪物火车》这类游戏着迷同时又对Godot引擎的强大与轻量有所耳闻那么“用Godot 4制作一个Roguelike卡牌构筑游戏”这个想法很可能已经在你脑海里盘旋很久了。这个项目标题——“Godot 4 Roguelike Deckbuilder教程项目指南”——精准地指向了一个充满魅力且极具实践价值的开发领域。它不仅仅是一个教程更是一个从零到一、系统性地构建一个复杂游戏类型的完整蓝图。为什么是Godot 4对于独立开发者和小团队而言Godot的开源、免费和极低的硬件需求是巨大的吸引力。Godot 4更是带来了革命性的渲染管线、改进的GDScript 2.0以及更强大的节点系统使得开发2D游戏的体验流畅无比。而Roguelike Deckbuilder卡牌构筑类Rogue这个类型融合了策略深度、随机性和重复可玩性其核心玩法循环——获取卡牌、构筑牌组、挑战关卡、死亡后带着新解锁内容重新开始——非常适合用Godot灵活的场景Scene和资源Resource系统来模块化实现。本指南旨在为你拆解这个项目的每一个核心环节。无论你是刚接触Godot的新手还是有一定基础想挑战更复杂系统的开发者你都将通过这个项目深入理解如何用Godot 4设计游戏数据架构、实现卡牌战斗逻辑、管理随机生成系统并最终打包成一个可玩的游戏原型。我们将避开泛泛而谈直接深入到那些决定项目成败的细节里比如如何用Resource定义一张卡牌的所有属性如何设计一个可扩展的“遗物”Relic系统以及如何处理层叠复杂的状态效果。让我们开始吧。2. 核心系统设计与架构思路构建一个卡牌构筑Roguelike远不是简单地把一些UI按钮和卡牌图片拼在一起。它需要一个深思熟虑的架构来支撑其复杂的数据流和状态管理。在Godot中这意味着要充分运用其节点Node与场景Scene的树形结构以及资源Resource的序列化优势。2.1 数据驱动一切皆可Resource游戏的核心数据——卡牌、遗物、敌人、状态效果——都应该被设计为继承自Resource的类。这是本项目最重要的设计决策之一。为什么选择Resource首先可序列化与独立存储。每个CardResource、EnemyResource都可以保存为独立的.tres或.res文件。你可以在Godot编辑器中可视化地编辑一张卡牌的伤害、费用、描述文字和特效脚本无需硬编码。这极大地提升了内容创作和平衡性调整的效率。 其次引用与实例分离。一个CardResource定义了一张卡牌的模板。当玩家将这张卡牌加入牌库时你创建的是这个Resource的一个引用或一个轻量级的实例包含当前战斗中的临时状态如本场战斗伤害1。这种模式清晰地区分了“定义”和“实例”避免了数据混乱。 最后Godot编辑器集成。自定义Resource可以与编辑器插件结合创建出非常友好的数据编辑界面虽然本教程不深入插件开发但使用内置的export关键字暴露属性已经能获得不错的编辑体验。一个基础的CardResource可能长这样GDScript# card_resource.gd extends Resource class_name CardResource export var card_name: String export_multiline var description: String export var energy_cost: int 1 export var texture: Texture2D # 卡牌类型攻击、技能、能力 export_enum(Attack, Skill, Power) var card_type: String Attack # 特效脚本一个函数名或一个独立的GDScript路径 export var effect_script: String deal_damage export var effect_value: int 62.2 场景Scene组织模块化游戏流程Godot的场景化思维在这里大放异彩。我们将游戏的不同状态和界面拆分为独立的场景并通过一个主场景如Main.tscn进行管理和切换。地图/导航场景MapScene负责显示Roguelike的关卡推进通常是一系列节点代表普通战斗、精英战斗、商店、休息点、Boss组成的路径图。每个节点都是一个Area2D或Button点击后触发战斗或事件。战斗场景CombatScene游戏的核心循环。它应包含以下子节点PlayerUI显示玩家的能量、生命、牌库/弃牌堆/抽牌堆数量。EnemyContainer一个Node2D用于动态生成并摆放敌人实例。Hand一个HBoxContainer或自定义布局节点用于展示玩家当前手牌。每张手牌是一个CardUI场景的实例。DiscardPile和DrawPile通常是隐藏的Node只用于逻辑管理但可以有简单的UI显示数量。卡牌UI场景CardUI.tscn这是一个可复用的场景用于在任何地方显示一张卡牌。它通常包含一个Panel或TextureRect作为背景Label显示名称、费用、描述以及处理鼠标悬停、点击的事件。它应该接收一个CardResource作为参数来更新显示。遗物与状态图标遗物被动效果和战斗中的状态如脆弱、力量也建议做成小的UI场景可以动态添加到玩家或敌人UI上。这种模块化设计使得调试、迭代和扩展变得非常容易。例如你可以单独在战斗场景中测试卡牌交互而不用每次都跑完整的地图流程。2.3 全局状态管理单例Autoload的运用游戏中有大量需要跨场景访问的数据玩家的当前生命、最大生命、金币、拥有的遗物列表、已解锁的卡牌池、当前运行的牌库等等。Godot的自动加载Autoload单例模式是管理这些全局状态的绝佳选择。你应该创建一个名为GameState.gd或Global.gd的脚本并将其添加到项目设置Project Settings的AutoLoad中。这个单例将作为游戏的“数据中心”。# GameState.gd extends Node var player_max_health: int 80 var player_current_health: int 80 var gold: int 99 var relics: Array[RelicResource] [] var deck: Array[CardResource] [] # 当前牌组 var master_card_pool: Array[CardResource] [] # 所有已解锁卡牌 func add_card_to_deck(card_res: CardResource): deck.append(card_res) func save_run_data(): # 可以使用ResourceSaver保存当前进度到文件 pass在战斗场景中你可以通过GameState.player_current_health直接访问和修改玩家生命值在商店场景可以通过GameState.gold判断能否购买商品。这避免了在场景之间繁琐的信号传递和节点引用。注意虽然单例很方便但要警惕“单例依赖症”。不是所有数据都适合放在全局单例里。战斗中的临时状态如敌人当前生命、玩家本回合获得的临时力量应该由战斗场景或对应的角色类管理战斗结束后即清除。单例更适合存储持久化的、贯穿整个游戏流程的元数据。3. 核心模块实现详解有了顶层设计我们来深入实现几个最关键的模块。这些是卡牌构筑Roguelike的支柱。3.1 卡牌系统的完整实现卡牌系统不仅仅是UI更是游戏逻辑的载体。我们需要实现从数据到逻辑到交互的完整链条。1. 卡牌数据层Resource扩展上面的CardResource是一个起点。为了支持更复杂的特效我们可以引入“效果Effect”系统。与其把特效逻辑硬编码在卡牌资源里不如定义一个基础的EffectResource然后让卡牌包含一个效果数组。# effect_resource.gd extends Resource class_name EffectResource export var target: String enemy # enemy, player, all_enemies, self export var value: int 0 export var icon: Texture2D # 每个具体效果继承这个类并实现execute方法 func execute(caster, target): pass # 具体效果造成伤害 class DamageEffect extends EffectResource: func execute(caster, target): if target.has_method(take_damage): target.take_damage(value) # 在CardResource中 export var effects: Array[EffectResource] []2. 卡牌逻辑层Card逻辑节点创建一个Card节点或只是一个GDScript类它持有一个CardResource引用并负责在战斗中执行卡牌效果。这个类不处理UI只处理逻辑。# card.gd extends Node class_name Card var card_resource: CardResource var upgrade_level: int 0 # 升级状态 func play(caster, targets: Array): if not can_play(caster): return false # 扣除费用等 caster.spend_energy(card_resource.energy_cost) # 执行所有效果 for effect in card_resource.effects: var actual_targets _get_targets(effect.target, targets) for t in actual_targets: effect.execute(caster, t) # 触发“打出卡牌”相关遗物或状态 GameEvents.card_played.emit(self) return true func _get_targets(effect_target: String, selected_targets: Array): # 根据effect_target和玩家选择的targets解析出实际的目标数组 # 例如effect_target是all_enemies就返回所有敌人 pass3. 卡牌表现层CardUI场景这是一个Control节点场景负责将Card或CardResource的数据可视化并处理玩家的输入点击、拖拽、悬停。# card_ui.gd extends TextureButton onready var name_label: Label $NameLabel onready var cost_label: Label $CostLabel onready var desc_label: Label $DescLabel var card_data: Card func setup(card: Card): card_data card name_label.text card.card_resource.card_name cost_label.text str(card.card_resource.energy_cost) desc_label.text _generate_description(card.card_resource) # 根据卡牌类型设置不同背景色 modulate Color.WHITE if _can_afford() else Color.GRAY func _on_pressed(): if _can_afford(): # 发出信号通知战斗管理器“这张卡被尝试打出” get_parent().get_parent().card_selected_for_play.emit(self) func _can_afford() - bool: var combat_manager get_node(/root/CombatScene/CombatManager) return combat_manager.current_energy card_data.card_resource.energy_cost4. 手牌与牌堆管理在战斗场景中需要一个DeckManager节点。它负责初始化牌库从GameState.deck复制、洗牌、抽牌、管理抽牌堆、手牌、弃牌堆和消耗堆的循环。# deck_manager.gd extends Node var draw_pile: Array[Card] [] var discard_pile: Array[Card] [] var exhaust_pile: Array[Card] [] # 消耗牌堆 var hand: Array[CardUI] [] # 当前手牌的UI引用 func setup_battle(deck_list: Array[CardResource]): draw_pile.clear() for card_res in deck_list: var card Card.new() card.card_resource card_res draw_pile.append(card) shuffle_draw_pile() draw_initial_hand(5) func draw_cards(num: int): for i in range(num): if draw_pile.is_empty(): reshuffle_discard_into_draw() if draw_pile.is_empty(): # 牌全抽完了 break var card draw_pile.pop_back() # 创建CardUI实例添加到手牌区域并调用setup var card_ui_instance preload(res://ui/card_ui.tscn).instantiate() card_ui_instance.setup(card) $Hand.add_child(card_ui_instance) hand.append(card_ui_instance)实操心得牌堆洗牌的“伪随机”与“真随机”对于卡牌游戏洗牌算法很重要。Godot的Array.shuffle()使用全局随机数种子对于需要重放或确定性随机的场合比如录像回放可能不合适。一个更可控的做法是使用你自己的随机数生成器RandomNumberGenerator并传入固定的种子。例如在每一场战斗开始时用“关卡种子战斗场次”生成一个RNG对象专门用于本次战斗的所有随机操作洗牌、敌人行动随机等。这样只要种子相同整个战斗流程就可以完全复现便于调试和实现“种子挑战”之类的功能。3.2 战斗循环与敌人AI设计战斗是游戏的核心交互环节。我们需要一个清晰的回合制状态机。1. 战斗状态机CombatManager创建一个CombatManager节点作为战斗场景的大脑。它管理战斗状态玩家回合、敌人回合、胜利、失败控制回合流程。# combat_manager.gd extends Node enum CombatState { PLAYER_TURN, ENEMY_TURN, VICTORY, DEFEAT } var current_state: CombatState CombatState.PLAYER_TURN var current_energy: int 3 var max_energy: int 3 func start_player_turn(): current_state CombatState.PLAYER_TURN current_energy max_energy emit_signal(turn_started, player) deck_manager.draw_cards(5) # 抽牌 # 更新UI显示能量 update_energy_ui() func end_player_turn(): # 弃掉所有手牌 for card_ui in deck_manager.hand: deck_manager.discard_card(card_ui.card_data) deck_manager.hand.clear() # 切换到敌人回合 start_enemy_turn() func start_enemy_turn(): current_state CombatState.ENEMY_TURN emit_signal(turn_started, enemy) # 遍历所有敌人执行他们的行动 for enemy in get_tree().get_nodes_in_group(enemies): enemy.take_turn() # 敌人行动结束后检查是否有敌人死亡然后回到玩家回合 check_enemies_defeated() if current_state CombatState.PLAYER_TURN: # 战斗未结束 call_deferred(start_player_turn) # 使用call_deferred避免递归过深2. 敌人设计与AI每个敌人也是一个场景Enemy.tscn包含精灵、血条和逻辑脚本。敌人的数据生命、攻击、意图也建议用EnemyResource定义。敌人的AI通常在take_turn()方法中实现。一种简单而有效的模式是“意图Intent系统”类似于《杀戮尖塔》。每个敌人在回合开始前或上个回合结束时就计算好下个回合要做什么并显示一个图标给玩家。# enemy.gd extends CharacterBody2D var enemy_resource: EnemyResource var current_health: int var next_intent: Intent # Intent是一个自定义类包含行动类型和数值 func setup(res: EnemyResource): enemy_resource res current_health res.max_health determine_intent() func determine_intent(): # 简单的AI逻辑根据权重随机选择行动 var actions enemy_resource.possible_actions var weights enemy_resource.action_weights var total_weight 0 for w in weights: total_weight w var roll randi_range(1, total_weight) var cumulative 0 for i in range(actions.size()): cumulative weights[i] if roll cumulative: next_intent Intent.new(actions[i], calculate_value(actions[i])) update_intent_ui() # 在敌人头上显示意图图标 break func take_turn(): match next_intent.type: attack: deal_attack(next_intent.value) defend: add_block(next_intent.value) debuff: apply_debuff_to_player(next_intent.value) # 行动结束后立即决定下回合的意图 determine_intent()注意事项敌人AI的复杂性与可读性对于初期项目不建议实现过于复杂的AI如状态机、行为树。使用“权重随机意图预览”的机制既能给玩家提供策略思考的空间知道敌人下回合要做什么又易于实现和平衡。你可以为不同的敌人配置不同的行动池和权重比如“攻击型敌人”的“攻击”权重高“防御型敌人”的“加甲”权重高。随着项目成熟再考虑引入更复杂的AI逻辑。3.3 Roguelike元素地图、遗物与随机生成Roguelike的“肉鸽”魅力很大程度上来源于其随机性和永久成长。1. 地图爬塔系统地图可以是一个简单的线性或分支节点图。用一个MapNode场景代表地图上的一个点战斗、商店、宝藏、休息、Boss。MapManager单例负责记录玩家当前所在位置并在地图场景中高亮显示。# map_manager.gd (Autoload) extends Node var current_map: Array # 存储当前层的地图节点数据 var current_node_index: int 0 var available_paths: Array[int] [] # 当前节点可前往的下一个节点索引 func generate_new_map(floor_number: int): current_map [] # 根据层数生成一定数量的节点并随机分配类型 var node_count 10 floor_number for i in range(node_count): var node_data { index: i, type: _random_node_type(floor_number, i), completed: false, rewards: [] # 战斗胜利后的奖励 } current_map.append(node_data) # 生成节点之间的连接关系一个有向无环图 _generate_connections() # 玩家从第一个节点开始 current_node_index 0 available_paths _get_connections_for_node(0) func move_to_node(node_index: int): if node_index in available_paths: current_node_index node_index available_paths _get_connections_for_node(node_index) var node_data current_map[node_index] # 根据节点类型加载不同的场景 match node_data[type]: combat: SceneManager.transition_to(res://combat/combat_scene.tscn, {enemy_pool: floor_1}) shop: SceneManager.transition_to(res://ui/shop_scene.tscn) # ... 其他类型2. 遗物Relic系统遗物是永久或持续性的被动效果。和卡牌一样用RelicResource定义。关键在于遗物的效果如何全局生效。我们可以在GameState单例中维护一个遗物列表并在关键的游戏事件上发出信号Signal让遗物去监听并触发效果。# 在GameState.gd中 signal player_turn_started signal card_played(card) signal player_damaged(amount) signal enemy_defeated(enemy) var active_relics: Array[RelicResource] [] func add_relic(relic: RelicResource): active_relics.append(relic) # 遗物被添加时可以立即执行一次“on_obtain”效果 relic.on_obtain(self) # 在战斗管理器或其他地方触发事件 func _on_player_turn_start(): GameState.player_turn_started.emit() # 每个遗物都可以连接到这个信号 for relic in GameState.active_relics: if relic.has_method(on_player_turn_start): relic.on_player_turn_start(self) # 一个具体的遗物脚本 # relic_burning_blood.gd (继承RelicResource) extends RelicResource func on_obtain(game_state): print(获得遗物燃烧之血) func on_combat_end(game_state): # 战斗结束时回复6点生命 game_state.player_current_health min(game_state.player_current_health 6, game_state.player_max_health)3. 随机奖励生成战斗胜利后、打开宝箱后需要从卡池、遗物池中随机抽取几项作为奖励。这里的随机需要精心设计以确保游戏体验避免出现完全无用的选项。func generate_card_rewards(count: int 3) - Array[CardResource]: var all_cards GameState.master_card_pool.duplicate() var rewards [] # 1. 过滤根据当前牌组强度、流派可以智能过滤掉完全不搭的卡牌进阶功能 # 2. 加权稀有卡牌出现概率更低 for i in range(count): if all_cards.is_empty(): break # 简单实现按稀有度权重随机 var total_weight 0 for card in all_cards: total_weight card.rarity_weight # 假设CardResource有rarity_weight字段 var roll randi_range(1, total_weight) var cumulative 0 for j in range(all_cards.size()): cumulative all_cards[j].rarity_weight if roll cumulative: rewards.append(all_cards[j]) all_cards.remove_at(j) # 避免重复 break return rewards踩坑记录随机数的“种子”与“可预测性”在Roguelike游戏中完全随机的奖励有时会让玩家感到挫败。一个高级技巧是使用“伪随机分布”PRD或“保底机制”。例如每次未抽到稀有卡时下次抽到稀有的概率小幅提升直到抽到后重置。这能让玩家的体验更平滑。Godot的RandomNumberGenerator可以让你完全控制随机过程便于实现这类机制。同时记得在游戏开始时用系统时间或玩家输入的种子初始化RNG以确保每次运行都有不同的体验除非使用固定种子进行调试。4. 美术、UI与音效集成指南一个玩法出色的游戏也需要得体的外表和反馈。Godot 4在UI和音频方面提供了强大的工具。4.1 使用Control节点构建自适应UIGodot的Control节点如Panel、Label、Button、HBoxContainer是构建UI的基石。关键在于用好锚点Anchors和边距Margins以及容器Container。手牌区域使用HBoxContainer或FlowContainer作为父节点。当你动态添加CardUI场景实例为其子节点时容器会自动排列它们。记得设置容器的Custom Minimum Size或调整子节点的Size Flags让卡牌能正确缩放和换行。生命/能量条可以使用TextureProgressBar。将血条背景图设置为Under纹理血条填充图设置为Progress纹理。在代码中更新其value属性即可。自适应布局对于需要占据屏幕特定比例的区域如底部手牌区占屏幕宽度80%在Theme Overrides中设置Custom Minimum Size或者使用Container的Anchor属性将其锚定到父容器的边缘。一个实用的UI技巧信号总线UI组件之间经常需要通信例如点击结束回合按钮通知战斗管理器。与其让节点之间直接引用不如建立一个“信号总线”单例SignalBus.gd定义所有全局UI信号。# SignalBus.gd (Autoload) extends Node signal end_turn_button_pressed signal card_hovered(card_ui) signal card_unhovered signal reward_selected(reward_type, reward_data) # 在EndTurnButton的脚本里 func _on_pressed(): SignalBus.end_turn_button_pressed.emit() # 在CombatManager里监听 func _ready(): SignalBus.end_turn_button_pressed.connect(_on_end_turn_pressed)这样极大地降低了UI节点与游戏逻辑节点的耦合度。4.2 2D美术与动画导入Godot支持多种图片格式PNG, JPEG, SVG等。对于像素风游戏PNG是最佳选择。将美术资源精灵图、背景、UI元素直接拖入Godot的FileSystem面板即可。精灵Sprite2D导入角色、敌人、特效的精灵图。如果使用精灵表Sprite Sheet可以使用AnimatedSprite2D节点在Frames属性中创建新的SpriteFrames资源并划分动画。图集AtlasTexture为了提高性能可以将多个小图片打包成一张大图集然后在Godot中创建AtlasTexture资源来引用其中的某个区域。Godot 4的2D渲染器能很好地处理图集。简单的程序动画Godot的Tween节点非常适合创建补间动画。例如卡牌打出时的飞向敌人动画、伤害数字弹出动画。# 让一张卡牌UI飞到目标位置并消失 func animate_card_to_target(card_ui: Control, target_position: Vector2): var tween create_tween() tween.set_parallel(true) # 并行执行以下动画 tween.tween_property(card_ui, position, target_position, 0.3).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK) tween.tween_property(card_ui, scale, Vector2(1.5, 1.5), 0.2) tween.chain().tween_property(card_ui, scale, Vector2(0, 0), 0.1) # 链式动画先放大再缩小 tween.tween_callback(card_ui.queue_free) # 动画结束后销毁节点4.3 音效与背景音乐管理音效能极大提升游戏打击感。Godot的AudioStreamPlayer一次性音效和AudioStreamPlayer2D带空间感的音效使用起来很简单。音效池Audio Pool为了避免频繁播放音效时创建和销毁AudioStreamPlayer节点带来的开销可以创建一个音效池。这是一个包含多个AudioStreamPlayer节点的预加载场景需要播放音效时从池中取出一个空闲的播放器使用。# audio_manager.gd (Autoload) extends Node var available_players: Array[AudioStreamPlayer] [] const POOL_SIZE 10 func _ready(): for i in range(POOL_SIZE): var player AudioStreamPlayer.new() add_child(player) available_players.append(player) player.finished.connect(_on_player_finished.bind(player)) func play_sound(stream: AudioStream, volume_db: float 0.0): if available_players.is_empty(): return # 或者可以动态创建一个新的 var player available_players.pop_back() player.stream stream player.volume_db volume_db player.play() func _on_player_finished(player: AudioStreamPlayer): available_players.append(player) # 播放完毕回收到池中背景音乐使用单独的AudioStreamPlayer节点播放背景音乐并利用AudioStreamPlayer的bus属性将其分配到不同的音频总线Bus上以便在游戏设置中独立控制音乐和音效的音量。5. 项目优化、调试与发布准备当核心功能完成后优化和打磨是让游戏从“能玩”到“好玩”的关键。5.1 性能分析与优化点Godot内置了性能分析器Debugger - Profiler在游戏运行时可以监控帧时间、内存、对象数量等。对象池Object Pooling对于频繁创建和销毁的对象如伤害数字、特效粒子、卡牌UI在抽牌/弃牌时使用对象池。预实例化一定数量的对象需要时激活并设置位置/属性不需要时隐藏而非销毁。上述音效池就是对象池的一种应用。避免每帧查找节点get_node()或$NodePath在_process()或_physics_process()中频繁调用会有开销。在_ready()中获取并缓存常用节点的引用。纹理与图集确保纹理尺寸是2的幂次方如128x128, 256x256并使用合适的压缩格式在Import面板中设置。将大量小纹理合并成图集可以减少绘制调用draw call。GDScript代码优化避免在循环中创建临时数组或字典。对于复杂的数学计算如果性能成为瓶颈可以考虑使用GDExtensionC或GDScript的静态类型提示: int,: Array来提高效率。5.2 调试技巧与常见问题排查使用print()和断点Godot编辑器的调试器功能强大。在关键逻辑处添加print()语句是最快的调试方式。对于复杂逻辑可以设置断点在行号左侧点击然后以调试模式运行游戏程序会在断点处暂停你可以查看所有变量的当前值。场景树调试在“远程”Remote视图中你可以实时查看运行中游戏的场景树检查节点是否被正确添加或移除。常见问题1信号未连接或连接错误。这是Godot新手最常见的问题之一。双击检查信号连接确保发送者和接收者路径正确。使用SignalBus单例可以部分缓解这个问题。常见问题2资源加载失败。确保文件路径正确并且资源在res://目录下。使用preload(“res://path/to/scene.tscn”)在编译时加载或load(“res://path/to/resource.tres”)在运行时加载。preload失败会在编辑阶段报错更容易发现。常见问题3物理或碰撞检测异常。检查碰撞层Layer和掩码Mask设置是否正确。在2D物理中确保碰撞形状CollisionShape2D的大小和位置与视觉精灵匹配。5.3 构建与发布设置当游戏开发完毕你需要将其导出为可执行文件。导出预设Export Preset在项目 - 导出中添加一个预设如“Windows Desktop”。你需要下载并设置对应的导出模板Export Templates。Godot官网提供了各个平台的模板。配置图标和名称在导出预设中设置好应用程序名称、图标等信息。图标需要准备多种尺寸如1024x1024, 512x512, 256x256等。资源过滤在“资源Resources”选项卡通常选择“导出所有资源”。但如果你有仅用于编辑器的测试资源可以通过“过滤器”排除它们以减小包体。开始导出点击“导出项目”选择一个输出目录和文件名。Godot会打包所有资源并生成可执行文件。测试导出版本务必在导出后脱离Godot编辑器环境运行生成的可执行文件进行测试。很多路径或资源加载问题只在导出后才会暴露。发布到平台根据目标平台如Steam、Itch.io、Google Play的要求准备商店所需的宣传图、描述、视频等材料并按照平台指南上传你的游戏包。从最初的一个空项目到构建出包含卡牌、战斗、地图、遗物、随机生成的完整Roguelike Deckbuilder原型这个过程充满了挑战但也极具成就感。Godot 4以其直观的设计和强大的功能让这个过程的每一步都清晰可控。记住最重要的不是一开始就做出一个完美的游戏而是先做出一个“可玩”的循环然后在此基础上不断迭代、添加内容和打磨手感。当你第一次击败自己设计的Boss第一次构筑出一套超强的连锁Combo时那种感觉是无与伦比的。希望这份指南能为你铺平道路祝你开发顺利。如果在实践中遇到具体问题Godot活跃的社区和详尽的官方文档总是最好的求助场所。