
这次我们来看一个面向游戏开发初学者的实战项目使用 Godot 引擎复刻一款《土豆兄弟》Brotato风格的 2D 俯视角生存游戏。这个项目的重点不是让你从零开始设计复杂的游戏系统而是通过一个具体的、可运行的案例快速掌握 Godot 在 2D 游戏开发中的核心工作流和关键模块。如果你对独立游戏开发感兴趣想了解如何用 Godot 实现角色控制、敌人 AI、武器系统、升级机制和 Roguelike 元素这篇文章可以直接收藏。我们将从环境搭建开始一步步拆解游戏的核心功能实现并最终得到一个可以运行、可以扩展的游戏原型。整个过程不涉及复杂的 3D 渲染或底层图形学重点关注 2D 游戏开发的实用技巧和 Godot 引擎的高效用法。1. 核心能力速览能力项说明项目类型2D 俯视角生存 Roguelike 游戏原型技术栈Godot 4.x (GDScript)核心功能玩家移动与射击、敌人 AI 与波次生成、武器与升级系统、UI 与状态管理开发门槛无需深厚图形学基础具备基础编程逻辑即可上手运行环境Windows / macOS / LinuxGodot 4.x 编辑器输出目标可独立运行的 PC 端游戏原型可导出为 exe, app 等适合场景Godot 初学者实战、2D 游戏原型快速验证、Roguelike 机制学习2. 适用场景与使用边界这个项目主要适合以下几类开发者Godot 初学者通过一个完整的项目快速理解场景Scene、节点Node、脚本GDScript和信号Signal等核心概念的实际应用。2D 游戏爱好者希望了解如何构建一个类似《吸血鬼幸存者》、《土豆兄弟》的俯视角生存游戏框架。独立游戏原型师需要快速验证一个包含移动、战斗、成长系统的游戏核心循环是否有趣。它能解决什么问题提供一个结构清晰、代码可读的 Godot 4 项目模板。演示如何组织 2D 游戏中的常见模块玩家、敌人、子弹、UI。展示 Godot 内置的物理系统、动画系统、UI 系统在实战中的用法。实现一个可玩的、具备基本 Roguelike 元素波次、升级选择的游戏原型。它不适合什么场景大型商业项目直接复用这是一个教学原型代码结构和资源管理未做深度优化。追求极致性能的硬核动作游戏物理碰撞、敌人数量极大时可能需要性能优化。需要复杂 3D 效果或网络联机的游戏本项目完全基于 2D 模块。版权与合规提醒本项目为学习目的复刻的是《土豆兄弟》的游戏玩法机制而非美术资源、剧情或 IP。所有使用的美术、音效素材应为原创或使用明确允许商用的资源如 Kenney.nl, Itch.io 上的免费资源包。开发完成后若计划发布请确保所有素材拥有合法授权。3. 环境准备与前置条件开始之前请确保你的开发环境满足以下要求操作系统Windows 10/11, macOS 10.14, 或主流 Linux 发行版。Godot 引擎跨平台支持良好。Godot 引擎下载并安装Godot 4.x稳定版。建议从官方网站或 Steam 获取。官网下载访问 Godot 官网选择 “Download” - “Godot 4.x Stable”。版本选择对于本 2D 项目标准版.NET 版本可选即可无需渲染功能更强的版本。磁盘空间预留至少 500MB 空间用于安装引擎和存放项目资源。基础技能对编程有基本了解任何语言能理解变量、函数、循环等概念。GDScript 语法类似 Python上手很快。检查清单[ ] Godot 4.x 已安装并可正常启动。[ ] 在电脑上创建一个专门用于存放 Godot 项目的文件夹例如D:\Godot_Projects。[ ] 准备一些占位用的美术资源如 16x16 或 32x32 的 PNG 图片用于快速原型开发。可以使用简单的几何图形方块、圆形代替。4. 项目创建与基础结构启动 Godot 引擎我们将从零开始创建项目。4.1 创建新项目点击 “New Project” 按钮。输入项目名称例如BrotatoStyleDemo。选择项目存储路径指向你刚才创建的文件夹。渲染器选择对于 2D 项目选择 “Forward” 或 “Mobile” 均可性能差异不大。点击 “Create Edit”。4.2 规划项目文件夹结构一个清晰的结构有助于管理资源。在 Godot 的文件系统面板中创建以下文件夹res:// ├── actors/ # 活动实体 │ ├── player/ # 玩家相关场景和脚本 │ └── enemies/ # 敌人相关 ├── weapons/ # 武器和子弹场景 ├── ui/ # 用户界面场景 ├── scenes/ # 主场景、房间场景 ├── scripts/ # 全局或工具脚本 ├── assets/ # 美术、音效资源 │ ├── sprites/ │ ├── sounds/ │ └── fonts/ └── autoloads/ # 自动加载的单例脚本如游戏管理器你可以通过右键点击res://- “New Folder” 来创建。4.3 设置窗口与项目设置窗口尺寸进入Project - Project Settings...。在Display - Window下设置Width: 1152Height: 648Test Width/Height: 同上可选Stretch Mode:canvas_itemsStretch Aspect:keep这样设置是为了一个固定的 16:9 游戏视图并允许在不同分辨率下缩放。输入映射在Project Settings中切换到Input Map标签页。我们需要添加游戏所需的输入动作。点击 “Add New Action”输入move_right然后点击 “Add”。同样方法添加move_left,move_up,move_down,shoot。为move_right分配键盘按键D或Right。其他方向键同理。为shoot分配鼠标左键Mouse Button Left或键盘按键如Space。5. 核心模块实现玩家角色玩家是游戏的核心。我们将创建一个可移动、可射击的玩家角色。5.1 创建玩家场景右键点击actors/player文件夹选择 “New Scene”。选择CharacterBody2D作为根节点命名为Player。为Player节点添加子节点Sprite2D用于显示玩家形象。暂时可以给它一个默认的icon.svg或你自己的精灵图。CollisionShape2D用于物理碰撞。为其Shape属性选择一个CircleShape2D或RectangleShape2D并调整大小匹配精灵。Timer命名为ShootTimer用于控制射击间隔。选中根节点Player点击右侧 “Node” 标签页下的 “Script” 按钮为它创建一个新脚本保存为player.gd。5.2 编写玩家移动脚本 (player.gd)extends CharacterBody2D # 导出变量方便在编辑器中调整 export var speed: float 300.0 export var bullet_scene: PackedScene # 将在编辑器中关联子弹场景 export var fire_rate: float 0.3 # 每秒发射次数 onready var shoot_timer: Timer $ShootTimer func _ready(): # 设置射击计时器 shoot_timer.wait_time 1.0 / fire_rate shoot_timer.start() func _physics_process(delta): # 1. 获取输入向量 var input_vector : Vector2.ZERO input_vector.x Input.get_action_strength(move_right) - Input.get_action_strength(move_left) input_vector.y Input.get_action_strength(move_down) - Input.get_action_strength(move_up) input_vector input_vector.normalized() # 标准化防止斜向移动更快 # 2. 计算速度并移动 velocity input_vector * speed move_and_slide() func _process(delta): # 朝向鼠标 look_at(get_global_mouse_position()) func _on_shoot_timer_timeout(): if bullet_scene and Input.is_action_pressed(shoot): shoot() func shoot(): var bullet_instance bullet_scene.instantiate() # 设置子弹的初始位置和方向 bullet_instance.global_position global_position bullet_instance.rotation rotation # 子弹朝向与玩家相同 # 将子弹添加到当前场景树中 get_parent().add_child(bullet_instance)代码解析_physics_process每帧调用用于处理物理相关的逻辑如移动。我们使用CharacterBody2D的move_and_slide()方法处理碰撞和移动。_process每帧调用用于处理非物理逻辑这里让玩家精灵始终面向鼠标。_on_shoot_timer_timeout连接到ShootTimer的timeout信号。当计时器到期时如果按住射击键则调用shoot()。shoot()实例化子弹场景设置其位置和方向并添加到场景中。5.3 创建子弹场景在weapons/文件夹下新建场景根节点选择Area2D命名为Bullet。添加子节点Sprite2D,CollisionShape2D。为根节点Bullet添加脚本bullet.gd。extends Area2D export var speed: float 600.0 export var damage: int 1 func _ready(): # 子弹在2秒后自动销毁防止内存泄漏 await get_tree().create_timer(2.0).timeout queue_free() func _physics_process(delta): # 向前移动 position transform.x * speed * delta func _on_body_entered(body): # 如果碰撞到敌人这里假设敌人有“enemy”组 if body.is_in_group(enemy): # 调用敌人受伤函数后续实现 if body.has_method(take_damage): body.take_damage(damage) # 子弹命中后消失 queue_free()回到Player场景在编辑器中将bullet_scene属性关联到你刚创建的Bullet.tscn。6. 核心模块实现敌人与波次系统敌人需要自动寻路攻击玩家并按波次生成。6.1 创建基础敌人场景在actors/enemies/下新建场景根节点为CharacterBody2D命名为BasicEnemy。添加Sprite2D,CollisionShape2D,HealthBar一个ProgressBar节点需调整方向等子节点。为根节点添加脚本basic_enemy.gd。extends CharacterBody2D class_name BasicEnemy # 给个类名方便识别 export var max_health: int 10 export var speed: float 150.0 export var damage: int 1 var current_health: int var player_node: Node2D onready var health_bar: ProgressBar $HealthBar func _ready(): current_health max_health health_bar.max_value max_health health_bar.value current_health # 查找玩家节点假设玩家在场景中且名为“Player” player_node get_tree().get_first_node_in_group(player) if not player_node: print(Warning: Player node not found in group player) func _physics_process(delta): if not player_node: return # 简单的追逐AI朝向玩家移动 var direction (player_node.global_position - global_position).normalized() velocity direction * speed move_and_slide() # 也可以添加简单的避障逻辑 func take_damage(amount: int): current_health - amount health_bar.value current_health if current_health 0: die() func die(): # 死亡效果播放动画、产生经验球、发出死亡信号等 # 例如通知游戏管理器敌人死亡 GameManager.enemy_died() queue_free()记得将玩家节点添加到 “player” 组中。在Player场景的根节点上在 “Node” 面板的 “Groups” 选项卡中添加组名 “player”。6.2 实现波次生成器我们需要一个管理器来控制敌人何时生成、生成多少。在scenes/下创建新场景根节点为Node2D命名为WaveSpawner。为其添加脚本wave_spawner.gd。这个脚本将负责定义波次数据每波敌人数量、类型、生成间隔。在场景中预设的生成点Marker2D节点生成敌人。控制波次之间的等待时间。extends Node2D export var enemy_scenes: Array[PackedScene] [] export var spawn_points: Array[Marker2D] [] export var wave_data: Array[Dictionary] [ {enemy_count: 5, spawn_delay: 1.0}, {enemy_count: 8, spawn_delay: 0.8}, # ... 更多波次 ] var current_wave_index: int 0 var enemies_alive: int 0 onready var game_manager get_node(/root/GameManager) # 假设有全局管理器 func _ready(): start_next_wave() func start_next_wave(): if current_wave_index wave_data.size(): print(All waves completed!) return var wave wave_data[current_wave_index] spawn_wave(wave[enemy_count], wave[spawn_delay]) current_wave_index 1 func spawn_wave(count: int, delay: float): for i in range(count): if enemy_scenes.size() 0 and spawn_points.size() 0: var enemy_scene enemy_scenes[0] # 简单起见使用第一种敌人 var spawn_point spawn_points.pick_random() var enemy_instance enemy_scene.instantiate() enemy_instance.global_position spawn_point.global_position get_parent().add_child(enemy_instance) enemies_alive 1 # 连接敌人的死亡信号以便计数 if enemy_instance.has_signal(died): enemy_instance.died.connect(_on_enemy_died) await get_tree().create_timer(delay).timeout func _on_enemy_died(): enemies_alive - 1 if enemies_alive 0: # 所有敌人都被消灭可以开始下一波或显示升级选择 game_manager.wave_cleared()在主场景中放置几个Marker2D节点作为生成点并将它们拖入WaveSpawner节点的spawn_points属性数组中。7. 核心模块实现游戏管理器与 UI游戏管理器GameManager作为全局单例负责管理游戏状态分数、波次、玩家状态、控制游戏流程开始、结束、暂停和协调其他系统。7.1 创建自动加载的单例在autoloads/文件夹下创建脚本game_manager.gd。进入Project - Project Settings... - Autoload。点击 “Add”在 “Path” 中选择你刚创建的game_manager.gd在 “Name” 中输入GameManager注意大小写。点击 “Add”。这样GameManager就会在游戏启动时自动加载并可以在任何脚本中通过GameManager直接访问。extends Node # 游戏状态信号 signal score_changed(new_score) signal wave_changed(new_wave) signal player_health_changed(new_health) signal game_over var score: int 0: set(value): score value score_changed.emit(score) var current_wave: int 1: set(value): current_wave value wave_changed.emit(current_wave) var player_health: int 100: set(value): player_health value player_health_changed.emit(player_health) if player_health 0: game_over.emit() func _ready(): # 初始化 pass func add_score(points: int): score points func enemy_died(): add_score(10) # 每杀死一个敌人得10分 # 其他逻辑... func wave_cleared(): current_wave 1 # 显示升级选择UI等 show_upgrade_selection() func show_upgrade_selection(): # 这里可以暂停游戏并显示一个包含多个升级选项的UI get_tree().paused true # 实例化并显示升级UI场景 var upgrade_ui preload(res://ui/upgrade_selection.tscn).instantiate() get_tree().root.add_child(upgrade_ui)7.2 创建游戏主界面 UIGodot 的 UI 系统基于 Control 节点。我们将创建一个简单的 HUD。在ui/文件夹下创建新场景根节点为CanvasLayer命名为HUD。CanvasLayer可以确保 UI 始终绘制在最上层。在CanvasLayer下添加一个Control节点作为容器然后添加以下子节点Label命名为ScoreLabel用于显示分数。Label命名为WaveLabel用于显示当前波次。TextureProgressBar或ProgressBar命名为HealthBar用于显示玩家生命值。为HUD根节点添加脚本hud.gd。extends CanvasLayer onready var score_label: Label $Control/ScoreLabel onready var wave_label: Label $Control/WaveLabel onready var health_bar: ProgressBar $Control/HealthBar func _ready(): # 连接全局管理器的信号 GameManager.score_changed.connect(update_score) GameManager.wave_changed.connect(update_wave) GameManager.player_health_changed.connect(update_health) # 初始化显示 update_score(GameManager.score) update_wave(GameManager.current_wave) update_health(GameManager.player_health) func update_score(new_score: int): score_label.text Score: %d % new_score func update_wave(new_wave: int): wave_label.text Wave: %d % new_wave func update_health(new_health: int): health_bar.value new_health在你的主游戏场景中实例化这个HUD.tscn。8. 功能整合与效果验证现在我们将所有模块组合起来创建一个可玩的游戏原型。8.1 构建主游戏场景创建一个新场景作为主场景根节点可以是Node2D命名为Main。将以下场景实例化拖入为子节点Player.tscn(放在场景中央)WaveSpawner.tscnHUD.tscn你可能还需要添加一个TileMap节点作为背景地板以及一些StaticBody2D节点作为障碍物。在Project Settings - Application - Run中将主场景设置为Main.tscn。8.2 运行与基础测试点击编辑器顶部的 “运行” 按钮或按 F5。你应该能看到玩家角色出现在场景中。使用WASD或方向键可以移动角色。角色始终面向鼠标。按住鼠标左键或射击键角色会按固定频率朝鼠标方向发射子弹。WaveSpawner开始生成敌人如果设置了波次数据。敌人生成后会朝玩家移动。子弹击中敌人敌人血量减少血条更新生命值归零后敌人消失。屏幕左上角的 UI 会更新分数、波次和玩家生命值。测试要点移动与射击是否流畅射击方向是否正确碰撞检测子弹是否能正确击中敌人并触发take_damage敌人碰撞体是否合理AI 行为敌人是否持续追踪玩家遇到障碍物时行为如何目前是直接穿过后续可加导航网格波次系统一波敌人被消灭后是否会触发wave_cleared并进入下一波UI 响应分数、波次、生命值变化时UI 是否实时更新性能观察打开 Godot 编辑器底部的 “调试器” 面板切换到 “监视器” 标签页。观察 “帧时间ms” 和 “物理帧时间ms”。在数十个敌人和子弹同时存在时帧率是否保持稳定60 FPS 对应约 16.6ms8.3 扩展功能验证升级系统《土豆兄弟》的核心乐趣之一在于每波结束后的升级选择。我们来简单实现一个。创建升级选择 UI 场景 (ui/upgrade_selection.tscn)。它是一个覆盖全屏的半透明面板上面有 3 个按钮每个按钮代表一种升级如最大生命值、射击速度、移动速度。在game_manager.gd的show_upgrade_selection方法中我们已经暂停游戏并显示这个 UI。为每个升级按钮连接pressed信号在回调函数中应用升级效果然后关闭 UI 并恢复游戏。# 在 upgrade_selection.gd 中示例 func _on_health_upgrade_pressed(): GameManager.player_max_health 20 # 需要先在GameManager定义这个变量 GameManager.player_health GameManager.player_max_health close_upgrade_ui() func close_upgrade_ui(): get_tree().paused false queue_free()再次运行游戏消灭第一波敌人后游戏应暂停并弹出升级选择界面。选择一项后游戏继续玩家属性得到增强。9. 资源管理与性能优化建议当项目逐渐复杂时需要注意以下方面对象池Object Pooling频繁创建和销毁子弹、敌人会带来内存分配开销。可以实现一个简单的对象池预先实例化一定数量的对象使用时激活不用时禁用并放回池中。信号Signals解耦大量使用全局管理器GameManager作为中介传递消息可以减少节点之间的直接引用使代码更清晰。例如敌人死亡时发出died信号WaveSpawner和GameManager都可以连接这个信号而不是敌人直接调用它们的方法。资源预加载Preloading对于常用的场景如子弹、敌人可以在游戏开始时预加载到内存中。# 在 GameManager 中 var bullet_scene preload(res://weapons/bullet.tscn)使用图层Layer和蒙版Mask在CollisionObject2D如Area2D,CharacterBody2D的属性中合理设置Collision Layer和Collision Mask。例如让子弹只与敌人层碰撞敌人只与玩家和墙壁层碰撞可以避免不必要的碰撞计算。简化物理形状为精灵使用的碰撞形状应尽可能简单圆形、矩形、胶囊形避免使用复杂的凹多边形以提升物理性能。10. 常见问题与排查方法问题现象可能原因排查方式解决方案运行后黑屏只有背景色主场景未设置或摄像机缺失检查Project Settings - Application - Run - Main Scene路径是否正确。在主场景中确保有一个Camera2D节点。正确设置主场景并添加Camera2D节点。玩家无法移动输入映射未正确设置或脚本未处理输入1. 检查Project Settings - Input Map中动作名是否与脚本中Input.get_action_strength使用的名字完全一致。2. 在Player脚本的_physics_process中打印input_vector查看是否有值。修正输入映射名称或脚本中的动作名。确保_physics_process被调用。子弹不发射bullet_scene属性未关联或ShootTimer未启动1. 在Player场景编辑器中检查bullet_scene属性是否已拖入Bullet.tscn。2. 检查ShootTimer节点的Autostart是否勾选或在_ready中是否调用了start()。正确关联场景确保计时器启动。在_ready中打印bullet_scene检查是否为null。敌人不追踪玩家未找到玩家节点或player_node为null在敌人脚本的_physics_process中打印player_node。确保玩家根节点加入了 “player” 组。将玩家节点添加到 “player” 组。或在敌人脚本中使用其他可靠方式查找玩家如通过标签。碰撞未触发碰撞层/蒙版未设置或信号未连接1. 检查发生碰撞的两个节点如Bullet(Area2D) 和Enemy(CharacterBody2D)的Collision Layer/Mask是否有交集。2. 检查Bullet的_on_body_entered信号是否连接到其自身的body_entered信号。在编辑器或代码中正确设置碰撞层。确保信号已连接蓝色图标。UI 不更新信号未正确连接或路径错误1. 检查HUD.gd中_ready函数里的GameManager.signal_name.connect(...)是否拼写正确。2. 检查GameManager中定义的信号是否使用signal关键字声明了。确保信号名称、连接函数名完全一致。使用编辑器的信号连接工具可视化操作更可靠。游戏运行卡顿同时存在大量活动节点敌人、子弹打开调试器“监视器”观察“对象计数”和“帧时间”。可能是没有及时销毁节点如子弹未设置生命周期。为子弹、特效等临时对象设置Timer自动销毁。考虑实现对象池。简化碰撞形状和精灵图。11. 总结与下一步通过这个项目我们快速实现了一个《土豆兄弟》风格的 2D 生存游戏核心框架。你不仅学会了 Godot 的基本操作更重要的是掌握了如何将想法分解为可执行的模块玩家控制、敌人 AI、武器系统、波次逻辑、UI 交互和全局状态管理。最值得尝试的扩展方向更多武器与攻击模式实现散射枪、激光、近战武器、自动追踪导弹等。为武器设计不同的数据结构伤害、射速、子弹速度、特殊效果。丰富的升级系统设计一个包含数十种升级的池子每轮随机提供 3-4 个选择。升级可以影响玩家属性、武器属性甚至增加新的能力如召唤物、光环。敌人多样性创建不同行为模式的敌人快速近战、远程射击、自爆、分裂、BOSS。使用状态机State Machine来管理敌人的不同行为状态巡逻、追逐、攻击、死亡。地图与障碍物使用TileMap创建更复杂的地图添加可破坏的障碍物增加游戏的策略性。音效与视觉反馈为射击、击中、升级、敌人死亡等事件添加音效和简单的粒子效果GPUParticles2D能极大提升游戏体验。数据持久化使用Resource来定义武器、敌人、升级的数值方便平衡和调整。实现一个简单的存档系统记录最高分、解锁内容等。建议的后续学习路径深入 GDScript学习更高级的语言特性如信号、协程await/yield、自定义资源Resource。学习 Godot 动画系统使用AnimationPlayer和AnimationTree为角色制作流畅的骨骼动画或帧动画。了解 Shader尝试编写简单的 2D Shader 来实现溶解、闪烁、像素化等视觉效果。探索 Godot 插件在 AssetLib 中寻找 UI 框架、调试工具、地图编辑器等插件来提升开发效率。这个原型项目代码结构清晰每个模块都相对独立非常适合作为你第一个 Godot 项目的起点。动手修改它添加你自己的创意是学习游戏开发最有效的方式。建议收藏本文在遇到具体问题时回来查阅对应的模块实现和排查方法。