公司动态

Godot 4 实战:从零构建《土豆兄弟》风格 2D Roguelike 生存射击游戏

📅 2026/8/25 4:09:31
Godot 4 实战:从零构建《土豆兄弟》风格 2D Roguelike 生存射击游戏
这次我们来看一个面向游戏开发初学者的实战项目使用 Godot 引擎复刻一款《土豆兄弟》风格的 2D 生存射击游戏。这个项目不是空谈理论而是直接带你从零开始一步步实现核心玩法。如果你对独立游戏开发、Roguelike 元素、Godot 的 GDScript 编程感兴趣或者想找一个能跑起来的完整案例来学习这篇文章可以直接跟着做。《土豆兄弟》是一款典型的俯视角 Roguelike 生存游戏玩家控制角色在潮水般的敌人中生存通过击败敌人获取资源在每波间隙升级、购买武器和道具形成独特的 Build。我们将使用 Godot 4 来实现其核心循环角色移动与射击、敌人波次生成、武器系统、升级商店以及永久成长机制。本文会重点拆解如何用 Godot 高效构建这些模块。我们将从项目结构搭建开始到玩家控制器、敌人 AI、武器数据驱动、UI 交互最后整合成完整的游戏循环。整个过程会关注 Godot 4 在 2D 开发中的特性如CharacterBody2D、信号Signals、资源Resource系统、场景实例化Instancing以及如何组织代码以支持 Roguelike 的随机性和成长性。1. 核心能力速览能力项说明项目类型Godot 4 2D 游戏开发实战教程目标成果一个可运行的、包含核心玩法的《土豆兄弟》风格游戏原型技术栈Godot Engine 4.x, GDScript, 2D 物理与渲染核心功能玩家移动与射击、敌人波次生成与 AI、多种武器系统、升级与商店、永久成长Meta Progression开发门槛基础编程逻辑理解无需深厚游戏开发经验输出平台Windows / macOS / Linux / Web (通过 Godot 导出)适合读者想通过完整项目学习 Godot 的初学者对 Roguelike 游戏机制实现感兴趣的开发者2. 适用场景与使用边界这个项目主要服务于学习和原型验证。适合谁Godot 初学者希望通过一个中等复杂度的完整项目系统学习 Godot 的节点、场景、脚本和资源管理。独立游戏开发者需要快速验证一个俯视角射击 Roguelike 升级玩法的原型。游戏设计学习者想了解《土豆兄弟》这类游戏的核心循环战斗-资源-成长是如何通过代码实现的。能解决什么问题Godot 2D 工作流实践从空项目到可发布原型的完整路径。游戏系统模块化设计学习如何将复杂的游戏功能移动、战斗、经济、成长拆分为独立的、可复用的场景和脚本。数据驱动设计入门使用 Godot 的Resource来定义武器、敌人、升级等数据使游戏内容易于调整和扩展。不适合什么场景追求 AAA 级画面本文聚焦玩法原型美术资源使用简单形状或免费素材不涉及复杂着色器或高级渲染。直接商用这是一个教学项目核心玩法灵感来源于《土豆兄弟》直接用于商业发布需注意玩法差异化和版权风险应作为学习基础进行深度创新。替代系统学习建议先了解 Godot 界面和 GDScript 基础语法再通过本项目深化理解。3. 环境准备与前置条件开始前请确保你的开发环境就绪。1. 操作系统Windows 10/11, macOS 10.14, 或主流 Linux 发行版。Godot 跨平台支持良好。2. Godot 引擎下载并安装Godot 4.2或更高版本稳定版。建议从官方网站或 Steam 获取。版本确认打开 Godot在编辑器右下角查看版本号。3. 磁盘空间Godot 编辑器本身约 100MB。项目文件代码、简单资源预计需要 50-200MB。确保有足够空间存放导出后的游戏包。4. 基础知识准备GDScript 基础了解变量、函数、控制流if/for、类的基本概念。Godot 文档有优秀的中文教程。Godot 编辑器界面熟悉场景树Scene Tree、检查器Inspector、文件系统FileSystem面板。2D 坐标概念了解 2D 坐标系X, Y、位置Position、旋转Rotation等。5. 可选辅助工具图像编辑软件如 Aseprite, GIMP, Krita用于制作或编辑简单的精灵Sprite。音频编辑软件如 Audacity用于处理音效。代码编辑器Godot 内置脚本编辑器足够也可配置外部编辑器如 VSCode。4. 项目结构与核心场景搭建一个清晰的项目结构是成功的一半。我们先在 Godot 中创建文件夹并搭建核心场景。步骤 1创建新项目打开 Godot点击“新建项目”。输入项目名称例如PotatoBrosClone。选择项目路径。渲染器选择对于 2D 项目选择“兼容性”渲染器即可它对硬件要求更低兼容性更好。点击“创建并编辑”。步骤 2创建文件夹结构在“文件系统”面板中创建以下文件夹让资源管理井井有条res:// ├── actors/ # 角色相关场景和脚本 │ ├── player/ │ └── enemies/ ├── weapons/ # 武器场景、脚本和数据 ├── ui/ # 用户界面场景 ├── scenes/ # 管理场景如主游戏、菜单 ├── resources/ # 资源文件如武器定义、升级定义 ├── scripts/ # 通用工具脚本 ├── audio/ # 音效和音乐 └── textures/ # 图片素材步骤 3创建主游戏场景 (Main.tscn)这是游戏运行的核心容器。右键scenes/文件夹选择“新建场景”。添加一个Node2D节点作为根节点重命名为Main。保存场景为scenes/Main.tscn。在Main节点下添加以下子节点它们将作为不同功能的“层”YSort节点重命名为World用于管理玩家、敌人等游戏实体YSort能根据节点的 Y 坐标自动排序渲染顺序实现角色互相遮挡的效果。CanvasLayer节点重命名为UI用于存放所有用户界面元素确保 UI 始终显示在最上层。Node2D节点重命名为Projectiles专门管理所有子弹、飞行物。Node2D节点重命名为Items管理地上掉落的资源、经验球等。将scenes/Main.tscn设为项目的主场景项目设置 - 应用 - 运行 - 主场景。5. 玩家角色实现移动与基础属性玩家是游戏的核心。我们来创建可移动、有生命值和经验的玩家角色。步骤 1创建玩家场景 (Player.tscn)在actors/player/文件夹中新建场景根节点选择CharacterBody2D命名为Player。为Player节点添加子节点Sprite2D用于显示玩家形象。可以先放一个简单的ColorRect矩形或从textures/导入一个图片。CollisionShape2D赋予碰撞形状如CircleShape2D半径根据精灵大小调整。Timer节点重命名为ShootTimer用于控制射击间隔。步骤 2编写玩家脚本 (Player.gd)为Player根节点附加新脚本保存为Player.gd。extends CharacterBody2D # 玩家属性 - 通过导出变量方便在编辑器中调整 export var speed: float 300.0 export var max_health: float 100.0 export var health: float max_health: set(value): health clamp(value, 0, max_health) health_changed.emit(health, max_health) # 发出信号更新UI if health 0: die() # 经验与等级 var experience: float 0.0 var level: int 1 var exp_to_next_level: float 100.0 # 信号用于与其他系统通信 signal health_changed(current_health, max_health) signal experience_changed(current_exp, exp_to_next) signal player_died func _physics_process(delta): # 1. 获取输入 var input_direction Input.get_vector(move_left, move_right, move_up, move_down) # 2. 计算速度 velocity input_direction * speed # 3. 移动并处理碰撞 move_and_slide() # 4. 朝向鼠标用于射击方向 look_at(get_global_mouse_position()) func take_damage(amount: float): health - amount func gain_experience(amount: float): experience amount experience_changed.emit(experience, exp_to_next_level) # 检查升级 if experience exp_to_next_level: level_up() func level_up(): experience - exp_to_next_level level 1 exp_to_next_level * 1.5 # 下一级所需经验增加 experience_changed.emit(experience, exp_to_next_level) # 这里可以触发升级选择UI print(Level Up! Now level: , level) func die(): player_died.emit() # 例如显示游戏结束画面重置游戏 queue_free()步骤 3配置输入映射在项目设置 - 输入映射中添加以下动作Actionmove_left 按键 A / 左箭头move_right按键 D / 右箭头move_up 按键 W / 上箭头move_down 按键 S / 下箭头shoot 鼠标左键步骤 4将玩家实例化到主场景打开scenes/Main.tscn。将actors/player/Player.tscn拖拽到World(YSort) 节点下。运行游戏现在你应该可以用 WASD 移动矩形并且矩形会旋转朝向鼠标。6. 武器系统数据驱动与射击逻辑武器是游戏玩法的关键。我们将设计一个数据驱动的武器系统便于添加新武器。步骤 1创建武器资源WeaponResource.gd)使用 Godot 的Resource类来定义武器属性。在scripts/文件夹右键创建新脚本继承自Resource。# WeaponResource.gd extends Resource class_name WeaponResource export var weapon_name: String Pistol export var damage: float 10.0 export var attack_speed: float 1.0 # 每秒攻击次数 export_range(1, 1000) var range: float 500.0 export var projectile_scene: PackedScene # 子弹场景 export var texture: Texture2D # 武器图标保存后你可以在 Inspector 中创建新的.tres资源文件来定义具体武器。步骤 2创建子弹场景 (BasicProjectile.tscn)在weapons/文件夹新建场景根节点为Area2D用于检测碰撞命名为BasicProjectile。添加子节点Sprite2D显示子弹、CollisionShape2D圆形或矩形。附加脚本# BasicProjectile.gd extends Area2D export var speed: float 800.0 export var damage: float 10.0 var direction: Vector2 Vector2.RIGHT func _physics_process(delta): position direction * speed * delta # 简单边界检查超出屏幕后删除 var viewport_rect get_viewport_rect() if !viewport_rect.has_point(global_position): queue_free() func _on_body_entered(body): if body.is_in_group(enemies): body.take_damage(damage) queue_free() # 击中后消失连接Area2D的body_entered信号到_on_body_entered函数。步骤 3修改玩家脚本以支持武器射击在Player.gd中添加武器相关逻辑# Player.gd 新增部分 # ... export var current_weapon: WeaponResource var can_shoot: bool true func _process(delta): if Input.is_action_pressed(shoot) and can_shoot and current_weapon: shoot() can_shoot false get_node(ShootTimer).wait_time 1.0 / current_weapon.attack_speed get_node(ShootTimer).start() func shoot(): if not current_weapon or not current_weapon.projectile_scene: return var projectile_instance current_weapon.projectile_scene.instantiate() # 设置子弹初始位置和方向 projectile_instance.global_position global_position projectile_instance.direction (get_global_mouse_position() - global_position).normalized() # 将子弹添加到主场景的 Projectiles 层 get_tree().root.get_node(Main/Projectiles).add_child(projectile_instance) func _on_shoot_timer_timeout(): can_shoot true记得将ShootTimer的timeout信号连接到_on_shoot_timer_timeout函数。步骤 4创建并配置一把武器在resources/文件夹右键选择“新建资源”搜索并创建WeaponResource。将其命名为pistol.tres。在 Inspector 中设置属性damage15,attack_speed2,range600。将projectile_scene指向你创建的BasicProjectile.tscn。回到Player场景在 Inspector 中将Current Weapon属性设置为刚创建的pistol.tres。运行游戏按住鼠标左键玩家应该会朝鼠标方向发射子弹。7. 敌人系统波次生成与简单AI敌人需要自动移动并攻击玩家。我们将实现一个简单的波次管理系统。步骤 1创建敌人场景 (Enemy.tscn)在actors/enemies/中新建场景根节点为CharacterBody2D命名Enemy。添加Sprite2D,CollisionShape2D。附加脚本# Enemy.gd extends CharacterBody2D export var speed: float 150.0 export var health: float 30.0 export var damage_to_player: float 10.0 export var experience_value: float 10.0 var player: Node2D func _ready(): # 尝试获取玩家引用 player get_tree().get_first_node_in_group(player) add_to_group(enemies) # 添加到“敌人”组便于子弹识别 func _physics_process(delta): if not player: return # 简单AI朝玩家移动 var direction (player.global_position - global_position).normalized() velocity direction * speed move_and_slide() func take_damage(amount: float): health - amount if health 0: die() func die(): if player and player.has_method(gain_experience): player.gain_experience(experience_value) queue_free()步骤 2创建波次生成器 (Spawner.gd)这是一个控制敌人生成逻辑的脚本可以附加到Main场景的一个Node2D上。# Spawner.gd extends Node2D export var enemy_scenes: Array[PackedScene] [] export var spawn_radius: float 400.0 export var time_between_waves: float 30.0 export var enemies_per_wave_base: int 5 export var wave_increase_factor: float 1.2 var current_wave: int 0 var enemies_alive: int 0 var spawn_timer: Timer var wave_timer: Timer func _ready(): spawn_timer Timer.new() add_child(spawn_timer) spawn_timer.one_shot true wave_timer Timer.new() add_child(wave_timer) wave_timer.wait_time time_between_waves wave_timer.timeout.connect(start_next_wave) wave_timer.start() start_next_wave() func start_next_wave(): current_wave 1 var enemy_count ceil(enemies_per_wave_base * pow(wave_increase_factor, current_wave - 1)) print(Wave %s started. Spawning %s enemies. % [current_wave, enemy_count]) for i in range(enemy_count): spawn_timer.wait_time i * 0.5 # 每隔0.5秒生成一个避免同时出现 spawn_timer.timeout.connect(_spawn_single_enemy.bind()) spawn_timer.start() await spawn_timer.timeout spawn_timer.timeout.disconnect(_spawn_single_enemy) func _spawn_single_enemy(): if enemy_scenes.is_empty(): return var enemy_scene enemy_scenes.pick_random() var enemy_instance enemy_scene.instantiate() # 在玩家周围随机位置生成 var player get_tree().get_first_node_in_group(player) if player: var spawn_direction Vector2.RIGHT.rotated(randf_range(0, TAU)) var spawn_distance randf_range(spawn_radius * 0.8, spawn_radius) enemy_instance.global_position player.global_position (spawn_direction * spawn_distance) get_parent().get_node(World).add_child(enemy_instance) # 添加到World层 enemies_alive 1 enemy_instance.tree_exiting.connect(_on_enemy_died) func _on_enemy_died(): enemies_alive - 1 if enemies_alive 0: print(Wave %s cleared! % current_wave) # 所有敌人都被消灭开始下一波倒计时 wave_timer.start()步骤 3集成到主场景打开Main.tscn在根节点下添加一个Node2D重命名为Spawner并将Spawner.gd脚本附加给它。在Spawner节点的 Inspector 中将Enemy Scenes数组添加一个元素并指向你创建的Enemy.tscn。运行游戏。你会看到敌人在玩家周围生成并朝玩家移动。击败敌人后玩家会获得经验。8. UI 系统生命值、经验条与升级选择UI 是玩家与游戏交互的窗口。我们将创建显示生命值、经验的常驻 UI以及一个升级选择界面。步骤 1创建 HUD 场景 (HUD.tscn)在ui/文件夹新建场景根节点为CanvasLayer命名HUD。添加控件节点TextureRect作为背景可选。Label节点重命名为HealthLabel用于显示生命值文本。ProgressBar节点重命名为HealthBar用于显示生命值进度。Label节点重命名为LevelLabel显示当前等级。ProgressBar节点重命名为ExpBar显示经验进度。附加脚本# HUD.gd extends CanvasLayer onready var health_label: Label $HealthLabel onready var health_bar: ProgressBar $HealthBar onready var level_label: Label $LevelLabel onready var exp_bar: ProgressBar $ExpBar func _ready(): # 初始隐藏等待玩家连接 hide() # 假设玩家节点有“player”组 var player get_tree().get_first_node_in_group(player) if player: connect_to_player(player) func connect_to_player(player: Node): player.health_changed.connect(update_health) player.experience_changed.connect(update_experience) # 初始化显示 update_health(player.health, player.max_health) update_experience(player.experience, player.exp_to_next_level) show() func update_health(current: float, max_health: float): health_label.text HP: %d/%d % [current, max_health] health_bar.max_value max_health health_bar.value current func update_experience(current: float, to_next: float): exp_bar.max_value to_next exp_bar.value current level_label.text Lv. %d % get_tree().get_first_node_in_group(player).level将HUD.tscn实例化为Main.tscn中UI节点的子节点。步骤 2创建升级选择界面 (UpgradeSelection.tscn)这是一个弹窗当玩家升级时出现提供多个升级选项。在ui/文件夹新建场景根节点为PanelContainer命名UpgradeSelection。添加VBoxContainer内部添加一个Label显示“选择升级”和三个Button代表三个升级选项。附加脚本# UpgradeSelection.gd extends PanelContainer signal upgrade_selected(upgrade_id: String) onready var option_buttons: Array[Button] [ $VBoxContainer/Option1, $VBoxContainer/Option2, $VBoxContainer/Option3 ] func display_upgrades(upgrades: Array): hide() # 默认隐藏 # 这里假设upgrades是一个包含升级ID和描述的数组 for i in range(min(upgrades.size(), option_buttons.size())): option_buttons[i].text upgrades[i].description option_buttons[i].pressed.connect(_on_option_selected.bind(upgrades[i].id)) show() func _on_option_selected(upgrade_id: String): upgrade_selected.emit(upgrade_id) hide() # 选择后隐藏在Main.gd需要为Main节点创建脚本中监听玩家的升级事件并弹出此界面。9. 游戏循环整合与状态管理现在我们需要一个“大脑”来协调所有系统游戏开始、结束、暂停、波次状态等。步骤 1创建游戏状态管理器 (GameManager.gd)为Main节点创建脚本。# Main.gd (附加给Main场景根节点) extends Node2D onready var player $World/Player onready var hud $UI/HUD onready var upgrade_ui $UI/UpgradeSelection enum GameState { PLAYING, PAUSED, GAME_OVER } var current_state: GameState GameState.PLAYING func _ready(): # 连接信号 player.player_died.connect(_on_player_died) if hud.has_method(connect_to_player): hud.connect_to_player(player) # 初始化升级UI隐藏 upgrade_ui.hide() func _on_player_died(): current_state GameState.GAME_OVER print(Game Over!) # 显示游戏结束画面这里可以实例化一个GameOver场景 # get_tree().paused true func _input(event): if event.is_action_pressed(pause): match current_state: GameState.PLAYING: current_state GameState.PAUSED get_tree().paused true # 显示暂停菜单 GameState.PAUSED: current_state GameState.PLAYING get_tree().paused false # 隐藏暂停菜单步骤 2定义升级系统在resources/中创建UpgradeResource.gd类似武器资源定义升级效果如增加伤害、生命值、速度等。当玩家升级时从预定义的升级池中随机选取几个显示在UpgradeSelection界面玩家选择后将效果应用到玩家属性上。10. 资源占用、性能观察与导出性能观察Godot 内置了性能监控工具。在编辑器运行时点击底部“调试器”面板切换到“监视器”选项卡。重点关注FPS确保稳定在60左右。如果过低检查物理对象数量、粒子效果或复杂脚本逻辑。2D 节点数大量敌人、子弹同时存在时节点数会上升。考虑使用对象池Object Pooling回收子弹和敌人。内存观察是否有持续增长防止内存泄漏。优化建议对象池为子弹和敌人创建对象池避免频繁instantiate()和queue_free()造成的性能开销。精灵批处理确保使用相同纹理的精灵Sprite2D启用了“纹理重复”或使用MultiMeshInstance2D进行批量渲染。简化碰撞使用简单的CircleShape2D或RectangleShape2D避免复杂多边形碰撞。按需生成敌人只在玩家视野附近生成。项目导出点击顶部菜单“项目” - “导出”。点击“添加…”选择目标平台如“Windows 桌面”。配置导出选项通常默认即可。点击“导出项目”选择输出路径和文件名如PotatoBrosClone.exe。导出的可执行文件可以独立运行分享给他人测试。11. 常见问题与排查方法问题现象可能原因排查方式解决方案运行游戏后黑屏只有背景色主场景未设置或玩家/相机未正确放置检查“项目设置 - 应用 - 运行 - 主场景”路径是否正确检查主场景中是否有Camera2D节点。正确设置主场景确保玩家或某个节点下有Camera2D并设置为“当前”。玩家无法移动输入映射未正确设置脚本中获取输入的动作名写错_physics_process未调用move_and_slide检查项目设置中的输入映射检查脚本中Input.get_vector使用的动作名是否与映射一致确认在_physics_process中调用了move_and_slide()。核对并修正输入动作名确保移动逻辑在_physics_process中。子弹不发射或方向错误ShootTimer未启动或连接子弹实例化后未添加到场景树子弹方向计算错误打印can_shoot变量状态检查子弹实例化后其global_position和direction使用print调试方向向量。确保计时器信号正确连接确认将子弹添加到了正确的父节点如Main/Projectiles检查方向计算逻辑。敌人不生成或生成位置不对Spawner脚本未正确附加或启用未找到玩家节点生成坐标计算有误检查Spawner节点是否在场景中且脚本已附加打印player变量是否为空打印生成的坐标。确保玩家被添加到“player”组检查生成半径和方向计算逻辑。升级界面不弹出升级信号未连接UI 场景未实例化或路径错误检查玩家level_up函数中是否发出了信号检查Main场景中UpgradeSelection节点的路径是否正确。正确连接信号使用get_node()或onready确保获取到正确的 UI 节点引用。游戏运行越来越卡节点数量过多子弹、敌人未及时清理内存泄漏使用调试器的“监视器”查看节点数和内存使用趋势检查是否有节点不断创建但未释放。实现对象池确保子弹超出屏幕或击中后、敌人死亡后调用queue_free()。导出后无法运行或缺少资源导出时未包含所有必要文件使用了绝对路径检查导出对话框的“资源”选项卡确保所有用到的场景、脚本、图片、字体都被包含。在导出设置中确保“过滤器”为非排除模式或手动添加遗漏的资源文件夹。12. 最佳实践与扩展方向最佳实践模块化设计保持场景和脚本的单一职责。玩家管移动和战斗生成器管刷怪UI 管显示。使用信号通信减少节点间的直接引用使用 Godot 强大的信号系统进行松耦合通信。数据驱动将武器、敌人、升级的数值定义在Resource文件中便于平衡和调整无需修改代码。版本控制使用 Git 管理项目特别是.gd脚本和.tscn场景文件。忽略.import文件夹和二进制导出文件。定期测试每实现一个功能就运行测试尽早发现问题。扩展方向更多武器与道具利用WeaponResource系统轻松添加霰弹枪、激光、导弹等并设计被动道具如增加生命恢复、投射物速度。敌人种类与行为树创建不同的敌人资源并实现更复杂的 AI如巡逻、逃跑、远程攻击可以使用StateMachine模式或BehaviorTree插件。地图与障碍物设计可破坏的障碍物、提供加成的神龛、随机房间等增加游戏层次。音效与音乐为射击、受伤、升级、敌人死亡等事件添加音效并配置背景音乐。本地化与存档实现多语言支持并使用Resource或文件来保存玩家的永久升级Meta Progression。发布与优化针对目标平台如 Web、移动端进行控件适配和性能优化考虑使用 Godot 的TextureAtlas和AudioStreamPool等高级功能。通过这个项目你不仅学会了 Godot 2D 开发的基本流程更重要的是掌握了构建一个完整游戏循环的思维方法。从移动射击到 Roguelike 成长每一个模块都可以作为你未来更大项目的基石。建议将代码和资源整理好尝试添加你自己的创意把它变成独一无二的游戏。