尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Godot 4 实战:从零构建《土豆兄弟》风格2D动作游戏原型

Godot 4 实战:从零构建《土豆兄弟》风格2D动作游戏原型 最近在尝试用 Godot 制作自己的独立游戏时发现很多教程要么过于基础要么直接跳到复杂的3D项目。对于想快速上手并做出一个完整、可玩的2D动作游戏的开发者来说中间似乎缺少一个“桥梁”。特别是像《土豆兄弟》这类俯视角、快节奏、带有Roguelite元素的游戏其核心玩法循环和手感实现是很多新手开发者渴望攻克的难点。本文将围绕使用 Godot 4 引擎从零开始复刻一个《土豆兄弟》风格的2D动作游戏原型。我们会从最基础的项目搭建讲起逐步实现角色移动、武器系统、敌人AI、波次生成、UI交互等核心模块。无论你是刚接触 Godot 的新手还是有一定基础想深入2D游戏开发的爱好者都能通过这篇教程掌握一套完整的、可复用的开发流程最终得到一个可以运行、可以扩展的游戏项目。1. 背景与核心概念为什么选择 Godot 开发 2D 游戏在开始敲代码之前我们先明确两个核心概念Godot 引擎和《土豆兄弟》类游戏的核心玩法。理解这些能帮助我们在开发中做出更合理的设计决策。1.1 Godot 引擎简介与优势Godot 是一个免费、开源、功能完整的跨平台游戏引擎。它最大的特点之一是节点Node与场景Scene的树形结构架构这种设计非常直观尤其适合2D游戏开发。对 2D 的原生友好Godot 拥有独立的 2D 渲染引擎其坐标系统、精灵Sprite处理、物理碰撞等都为 2D 开发做了深度优化使用起来比在 3D 引擎中模拟 2D 要简单高效得多。轻量级与高性能引擎本身非常小巧启动和运行速度快。其 GDScript 语言语法类似 Python学习曲线平缓能让你快速将想法转化为原型。强大的动画系统AnimationPlayer和AnimationTree节点让制作复杂的 2D 骨骼动画和状态机变得简单。活跃的社区与丰富的资源作为热门开源引擎Godot 拥有庞大的学习社区和不断增长的资源库包括插件、素材等遇到问题更容易找到解决方案。对于独立开发者和小团队来说Godot 在开发 2D 游戏方面的效率和灵活性是其非常吸引人的一点。1.2 《土豆兄弟》风格游戏的核心要素分析《土豆兄弟》是一款俯视角、动作射击、Roguelite 游戏。我们要复刻的“风格”主要指其核心的游戏体验循环而非美术资源。我们可以将其拆解为以下几个可实现的模块俯视角角色控制玩家使用键盘WASD控制角色在平面上的移动鼠标控制射击方向。这是操作手感的基础。武器与攻击系统角色持有武器如枪械能够向鼠标方向自动或手动发射子弹。涉及子弹实例化、飞行逻辑和伤害计算。敌人与 AI敌人会自主寻路并向玩家移动接触到玩家则造成伤害。需要简单的移动 AI 和碰撞检测。波次生存机制游戏以波次Wave形式进行一波敌人被消灭后下一波出现且难度敌人数量、种类逐渐提升。成长与 Roguelite 元素每波结束后玩家可以选择一项随机升级如增加伤害、攻速、移动速度等让角色随着游戏进程不断变强形成“战利品驱动”的正反馈循环。基础 UI显示玩家的生命值、当前波次、金币等关键信息。我们的目标就是使用 Godot一步步将这些模块构建起来形成一个可玩的游戏原型。2. 环境准备与项目初始化工欲善其事必先利其器。首先确保我们有一个统一的开发环境。2.1 软件安装与版本说明Godot 引擎访问 Godot 官网下载入口选择Godot 4.2.x稳定版本。建议下载“Standard”版本。本文所有示例均基于 Godot 4.2.1。操作系统Windows 10/11, macOS 或 Linux 均可。Godot 的跨平台特性保证了项目在不同系统上的一致性。代码编辑器Godot 内置的脚本编辑器已足够好用。你也可以配置外部编辑器如 VSCode但这并非必需。美术与音效素材为了专注于编程逻辑我们可以使用免费的占位素材。推荐在 itch.io 或 OpenGameArt 网站搜索 “top-down”、“pixel art” 等关键词下载一些简单的精灵图Sprite和音效。重要提示Godot 4.x 与 3.x 在脚本语法、部分 API 和项目设置上有较大差异。请务必确认你使用的是 4.x 版本否则代码可能无法运行。2.2 创建新项目与初始设置打开 Godot点击“新建项目”。输入项目名称例如PotatoBrosClone。选择一个空文件夹作为项目路径。渲染器选择对于纯 2D 项目选择Forward或Mobile均可。两者对 2D 支持都很好Forward 功能更全面。点击“创建并编辑”Godot 会打开一个空项目。首次进入后我们需要进行几项关键设置项目设置 - 显示 - 窗口将窗口的“宽度”和“高度”设置为你的目标分辨率例如 1152x648。勾选“可调整窗口”以便测试。项目设置 - 输入映射这里可以定义输入动作我们稍后会来配置。现在你的 Godot 编辑器界面应该包含“场景”、“文件系统”、“导入”、“节点”、“历史”等面板。我们主要的工作将在“场景”面板构建游戏对象树和“脚本”面板编写逻辑中完成。3. 核心系统拆解从节点树到游戏逻辑Godot 的游戏是由场景构成的而场景是由节点组成的树。理解以下几个核心节点类型是构建游戏的基础。3.1 关键节点类型介绍Node2D所有 2D 节点的基类。提供位置、旋转、缩放等 2D 变换属性。CharacterBody2D用于需要自定义移动逻辑如受玩家输入控制的 2D 角色。我们将用它制作玩家和敌人。Area2D用于检测区域。非常适合制作子弹的伤害区域、敌人的感知范围、拾取物品的范围等。CollisionShape2D/CollisionPolygon2D为物理体如CharacterBody2D或区域Area2D提供碰撞形状。没有碰撞形状物体就无法发生物理交互。Sprite2D用于显示 2D 纹理图像即我们的角色、敌人、子弹的图片。Timer一个非常实用的节点用于延迟执行或重复执行某个函数。例如控制武器的射击间隔。Camera2D2D 相机用于控制游戏视图的跟随和边界。3.2 GDScript 基础语法速览GDScript 是 Godot 的主要脚本语言类似 Python。# 定义一个变量 var health: int 100 # 显式类型声明 var player_name Potato # 类型推断 # 定义一个函数 func take_damage(amount: int) - void: health - amount if health 0: die() # 内置的 _ready 函数在节点进入场景树时调用一次 func _ready(): print(Player ready!) # 内置的 _process 函数每帧调用一次 func _process(delta: float): # delta 是上一帧到这一帧的时间间隔用于实现与帧率无关的运动 rotate(speed * delta) # 内置的 _physics_process 函数在物理帧默认每秒60次调用 func _physics_process(delta: float): # 处理物理相关的移动如使用 move_and_slide pass掌握这些基础后我们就可以开始构建第一个游戏场景了。4. 完整实战构建《土豆兄弟》风格游戏原型我们将按照模块顺序一步步搭建游戏。请跟随步骤在 Godot 中创建对应的场景和脚本。4.1 创建玩家角色目标创建一个可以用 WASD 移动、用鼠标控制朝向的CharacterBody2D。创建玩家场景在“文件系统”面板右键选择“新建场景”。添加一个CharacterBody2D节点作为根节点将其重命名为Player。为Player节点添加子节点Sprite2D用于显示角色图片和CollisionShape2D用于物理碰撞。为Sprite2D的“纹理”属性分配一个占位图片如一个圆形或正方形。为CollisionShape2D的“形状”属性新建一个CircleShape2D并调整半径以匹配精灵图大小。编写玩家移动脚本选中Player根节点点击“附加脚本”按钮创建一个新脚本命名为player.gd。编写移动和旋转逻辑# player.gd extends CharacterBody2D # 导出变量可以在编辑器的属性面板中直接调整 export var speed: float 300.0 export var rotation_speed: float 10.0 func _ready(): # 玩家初始化的代码可以写在这里 pass func _physics_process(delta): # 1. 获取输入向量 var input_vector : Vector2.ZERO input_vector.x Input.get_axis(move_left, move_right) # 需要先在输入映射中定义 input_vector.y Input.get_axis(move_up, move_down) input_vector input_vector.normalized() # 标准化防止斜向移动更快 # 2. 计算速度并移动 velocity input_vector * speed move_and_slide() # 执行移动并处理碰撞 # 3. 让角色朝向鼠标位置 var mouse_pos: Vector2 get_global_mouse_position() var direction: Vector2 (mouse_pos - global_position).normalized() if direction.length() 0: # 使用 look_at 函数让节点“看向”一个点90度是为了修正精灵默认朝向 look_at(mouse_pos) rotation_degrees 90 # 根据你的精灵图朝向调整配置输入映射打开项目设置 - 输入映射。添加以下四个动作Actionmove_left按键A。move_right按键D。move_up按键W。move_down按键S。这样Input.get_axis(“move_left”, “move_right”)就会返回 -1按A到 1按D之间的值。现在运行场景你应该可以控制角色移动并且角色会始终朝向鼠标光标。4.2 实现武器与子弹系统目标为玩家添加一把枪按下鼠标左键时从枪口位置发射子弹。创建子弹场景新建场景根节点为Area2D重命名为Bullet。为其添加子节点Sprite2D子弹图片和CollisionShape2D圆形或矩形碰撞体。选中Bullet根节点附加脚本bullet.gd。# bullet.gd extends Area2D export var speed: float 800.0 export var damage: int 10 var direction: Vector2 Vector2.RIGHT # 发射方向由武器设置 func _ready(): # 子弹在发射后一段时间自动消失防止内存泄漏 $Timer.start() # 假设你为Bullet场景添加了一个Timer子节点并设置了等待时间如1秒 func _physics_process(delta): # 沿着方向匀速移动 position direction * speed * delta func _on_timer_timeout(): queue_free() # 销毁自身 func _on_body_entered(body: Node2D): # 当子弹碰撞到物理体如敌人时 if body.is_in_group(enemies): # 我们稍后会给敌人添加“enemies”组 body.take_damage(damage) # 调用敌人的受伤函数 queue_free() # 碰撞后销毁子弹为玩家添加武器在Player场景中添加一个Marker2D节点作为子节点重命名为Muzzle枪口。将其位置调整到角色精灵的“枪口”处。修改player.gd添加射击逻辑# 在 player.gd 顶部添加变量 export var bullet_scene: PackedScene # 用于存放子弹场景 export var fire_rate: float 0.2 # 射击间隔秒 var can_fire: bool true # 在 _physics_process 函数末尾添加 func _physics_process(delta): # ... 原有的移动和旋转代码 ... # 射击逻辑 if Input.is_action_pressed(shoot) and can_fire: shoot() can_fire false # 使用 Timer 实现射击冷却更优雅的方式是添加一个Timer节点 # 这里简单使用 await 和 get_tree().create_timer await get_tree().create_timer(fire_rate).timeout can_fire true # 新增射击函数 func shoot(): if bullet_scene: var bullet_instance bullet_scene.instantiate() # 实例化子弹 get_parent().add_child(bullet_instance) # 添加到当前场景通常是主世界 bullet_instance.global_position $Muzzle.global_position bullet_instance.direction ($Muzzle.global_position - global_position).normalized() # 或者更精确bullet_instance.rotation rotation # bullet_instance.direction Vector2.RIGHT.rotated(rotation)连接与配置在Player节点的属性面板中将bullet_scene属性拖拽赋值为你创建好的Bullet.tscn场景。在输入映射中添加shoot动作绑定鼠标左键Mouse Button Left。现在运行游戏移动角色并点击鼠标左键应该可以看到子弹从枪口射出。4.3 创建敌人与简单 AI目标创建会自动朝玩家移动并造成接触伤害的敌人。创建敌人场景新建场景根节点为CharacterBody2D重命名为Enemy。添加Sprite2D、CollisionShape2D。添加一个Area2D节点作为子节点用于检测与玩家的接触伤害。为这个Area2D也配上CollisionShape2D。为根节点Enemy附加脚本enemy.gd。编写敌人 AI 脚本# enemy.gd extends CharacterBody2D export var speed: float 150.0 export var health: int 30 var player: Node2D null func _ready(): # 将敌人添加到“enemies”组方便子弹识别 add_to_group(enemies) # 尝试查找场景中的玩家 player get_tree().get_first_node_in_group(player) # 需要给玩家也添加组 func _physics_process(delta): if player: # 计算朝向玩家的方向 var direction (player.global_position - global_position).normalized() velocity direction * speed move_and_slide() # 简单朝向玩家 look_at(player.global_position) func take_damage(damage_amount: int): health - damage_amount # 可以在这里添加受伤特效或音效 if health 0: die() func die(): # 死亡处理播放动画、掉落物品、增加分数等 queue_free() # 暂时直接销毁 # 当敌人接触玩家的 Area2D 时调用需要连接信号 func _on_hitbox_body_entered(body): if body.is_in_group(player): body.take_damage(10) # 对玩家造成伤害信号连接选中敌人场景中的Area2D节点我们称之为Hitbox。在“节点”面板切换到“信号”标签页。双击body_entered信号连接到Enemy根节点并选择_on_hitbox_body_entered函数。给玩家添加组和受伤函数在Player场景的根节点属性中找到“组”标签添加一个名为player的组。在player.gd中添加take_damage函数和health变量。# 在 player.gd 顶部添加 var health: int 100 # 新增函数 func take_damage(amount: int): health - amount print(Player health: , health) # 暂时用打印代替UI显示 if health 0: game_over()4.4 实现波次生成系统目标游戏按波次进行每波生成一定数量的敌人全部消灭后进入下一波。创建游戏管理器新建场景根节点为Node重命名为GameManager。保存为GameManager.tscn。将其附加脚本game_manager.gd。这个节点将作为我们游戏的“大脑”。编写波次逻辑# game_manager.gd extends Node export var enemy_scene: PackedScene export var spawn_radius: float 400.0 # 在玩家周围多远生成敌人 var current_wave: int 1 var enemies_in_wave: int 5 var enemies_alive: int 0 var player: Node2D null func _ready(): player get_tree().get_first_node_in_group(player) start_wave() func start_wave(): print(Wave %d started! % current_wave) enemies_alive enemies_in_wave for i in range(enemies_in_wave): # 延迟生成每个敌人避免全部堆在一起 await get_tree().create_timer(0.5).timeout spawn_enemy() func spawn_enemy(): if enemy_scene and player: var enemy_instance enemy_scene.instantiate() get_parent().add_child(enemy_instance) # 添加到主场景 # 在玩家周围随机位置生成 var spawn_direction Vector2.RIGHT.rotated(randf_range(0, TAU)) # TAU 2 * PI var spawn_distance randf_range(100, spawn_radius) enemy_instance.global_position player.global_position (spawn_direction * spawn_distance) func _on_enemy_died(): # 需要在敌人死亡时调用此函数 enemies_alive - 1 print(Enemies remaining: , enemies_alive) if enemies_alive 0: wave_cleared() func wave_cleared(): print(Wave %d cleared! % current_wave) # 波次间休息 await get_tree().create_timer(3.0).timeout # 增强下一波 current_wave 1 enemies_in_wave 3 # 每波增加3个敌人 start_wave()修改敌人脚本以通知 GameManager在enemy.gd的die()函数中发出死亡信号或调用 GameManager 的函数。这里我们使用一个简单的全局查找方法对于原型项目足够。# 在 enemy.gd 的 die() 函数中 func die(): # 通知游戏管理器敌人死亡 var game_manager get_tree().get_first_node_in_group(game_manager) if game_manager and game_manager.has_method(_on_enemy_died): game_manager._on_enemy_died() queue_free()设置主场景新建一个场景作为主场景根节点可以是Node2D命名为Main。将Player.tscn、GameManager.tscn实例化进来。给GameManager节点的enemy_scene属性赋值Enemy.tscn。给GameManager节点添加一个game_manager组。将Main.tscn设为项目的主场景项目设置 - 应用 - 运行 - 主场景。现在运行游戏你会看到敌人一波波地生成消灭完一波后下一波敌人更多。4.5 添加 UI 与升级系统目标显示生命值和波次并在波次结束后提供随机升级选项。创建 UI 场景Godot 的 UI 使用Control节点。新建场景根节点为CanvasLayer确保UI显示在最上层重命名为UI。在其下添加Control节点然后使用Label节点来显示生命值和波次。创建一个升级选择面板Panel里面包含几个Button波次结束后显示。编写 UI 脚本# UI.gd extends CanvasLayer onready var health_label: Label $Control/HealthLabel onready var wave_label: Label $Control/WaveLabel onready var upgrade_panel: Panel $Control/UpgradePanel func _ready(): update_health(100) # 初始值 update_wave(1) upgrade_panel.hide() func update_health(value: int): health_label.text Health: %d % value func update_wave(value: int): wave_label.text Wave: %d % value func show_upgrades(upgrades: Array): # upgrades 是一个包含升级选项的数组 upgrade_panel.show() # 这里简化处理假设有三个按钮将upgrades数组的内容赋给按钮文本 for i in range(min(3, upgrades.size())): var button upgrade_panel.get_node(Button str(i1)) if button: button.text upgrades[i] # 需要连接按钮的 pressed 信号应用对应的升级效果 func _on_upgrade_selected(upgrade_index: int): # 处理升级逻辑例如增强玩家属性 print(Selected upgrade: , upgrade_index) upgrade_panel.hide() # 通知游戏管理器继续下一波 get_tree().call_group(game_manager, continue_to_next_wave)连接游戏管理器与 UI修改game_manager.gd在wave_cleared()函数中不再直接等待后开始下一波而是显示升级界面。# 在 game_manager.gd 中 func wave_cleared(): print(Wave %d cleared! % current_wave) # 显示升级界面 var ui get_tree().get_first_node_in_group(ui) if ui: var random_upgrades [10 Max Health, 20% Speed, 15% Damage] # 示例 ui.show_upgrades(random_upgrades) # 暂停敌人生成等待玩家选择升级 # set_process(false) 或使用一个状态变量来控制 func continue_to_next_wave(): # 被UI调用 current_wave 1 enemies_in_wave 3 start_wave()至此一个拥有核心玩法的《土豆兄弟》风格游戏原型就搭建完成了。玩家可以移动、射击敌人会一波波袭来波次间可以选择升级。5. 常见问题与排查思路在按照教程操作时你可能会遇到一些典型问题。这里列出一些常见坑点及其解决方法。问题现象可能原因排查与解决思路角色无法移动控制没反应1. 输入映射未正确设置。2._physics_process函数未重写或代码有误。3.CharacterBody2D的碰撞形状未设置或太小。1. 检查“项目设置-输入映射”中的动作名是否与脚本中Input.get_axis使用的字符串完全一致。2. 在_physics_process函数中添加print(input_vector)调试输出看是否获取到输入值。3. 在场景编辑器中检查CollisionShape2D是否可见且大小合适。子弹不发射或发射位置不对1.bullet_scene属性未赋值。2. 子弹实例化后添加到了错误的父节点。3.Muzzle节点的位置未调整。1. 确保在Player节点的属性面板中将Bullet.tscn拖拽到bullet_scene属性栏。2. 使用get_tree().current_scene.add_child(bullet_instance)或get_parent().add_child()确保子弹添加到主场景树。3. 在Player场景中调整Muzzle节点的位置到“枪口”。敌人不朝玩家移动1. 未成功获取到player引用。2. 玩家未添加到player组。3. 敌人的_physics_process中移动代码未执行。1. 在_ready()中打印player的值检查是否为null。2. 确认Player根节点已加入player组。3. 在_physics_process中打印direction向量看计算是否正确。碰撞检测不起作用1. 碰撞层Layer和掩码Mask未设置。2. 信号未正确连接。3.Area2D的Monitoring属性为false。1. 检查相关节点如Area2D,CharacterBody2D的“碰撞”属性确保层和掩码有重叠。例如子弹的Area2D掩码应包含敌人所在层。2. 在“节点”面板的“信号”标签页检查body_entered等信号是否显示为已连接。3. 确保Area2D的Monitoring和Monitorable属性为开启状态。游戏运行卡顿随着敌人增多越来越慢1. 子弹或敌人实例化后未及时销毁。2. 每帧执行的逻辑过于复杂。1. 确保子弹在命中或超出生存时间后调用queue_free()。确保敌人在死亡后调用queue_free()。2. 使用 Godot 的性能分析器调试器 - 分析器查看是哪部分代码或节点耗时最多。对于大量敌人可以考虑使用MultiMeshInstance2D进行合批渲染。6. 最佳实践与工程建议将原型打磨成一个更健壮、易扩展的项目需要遵循一些良好的实践。6.1 代码组织与架构使用信号Signal进行松耦合通信在上面的例子中我们用了“组”和直接函数调用来通信。对于更复杂的项目应多用信号。例如Enemy可以定义一个died信号GameManager连接这个信号而不是在敌人内部查找管理器。# 在 enemy.gd 中 signal died func die(): emit_signal(“died”) queue_free() # 在 game_manager.gd 中 func _on_enemy_spawned(enemy_instance): enemy_instance.connect(“died”, _on_enemy_died)将常量提取到独立脚本或配置文件中例如将玩家初始速度、敌人基础血量、波次计算公式等放到一个globals.gd或config.gd脚本中通过Global单例访问便于平衡和调整。使用状态模式管理复杂行为如果玩家或敌人的行为状态闲置、移动、攻击、受伤变得复杂可以考虑使用状态机来管理使代码更清晰。6.2 资源与场景管理场景实例化与内存管理使用PackedScene的instantiate()和queue_free()是标准做法。对于需要频繁创建销毁的对象如子弹可以考虑对象池Object Pooling预先创建一批对象循环使用以减少内存分配开销。合理使用资源唯一性对于多个敌人共用的材质、纹理确保在资源层面是唯一的而不是每个敌人都复制一份。6.3 性能优化绘制调用Draw Call优化Godot 的 2D 渲染器会自动进行批处理但条件有限。确保使用纹理图集Texture Atlas将多个小精灵图打包成一张大图可以显著减少绘制调用。物理性能简化碰撞形状。用CircleShape2D或RectangleShape2D代替复杂的CollisionPolygon2D除非必要。减少同时活动的物理体数量。使用VisibilityNotifier2D对于会在屏幕外活动的敌人如飞行射击游戏可以将其设为不可见或暂停处理以节省性能。6.4 扩展游戏性数据驱动设计将敌人属性血量、速度、伤害、武器属性、升级效果等定义为字典或资源文件如JSON或Resource。这样可以通过修改数据而非代码来调整游戏内容。实现更多 Roguelite 元素随机房间将游戏场地划分为房间每波在一个随机生成的房间内进行。多样化的武器和道具创建不同的武器场景霰弹枪、激光、近战武器并设计道具拾取系统。永久升级与局内升级结合引入金币系统局内升级仅在单局游戏内有效局外可以用金币购买永久性增强。通过遵循这些实践你的 Godot 2D 项目将不仅是一个可运行的原型更是一个结构清晰、易于维护和扩展的扎实工程为开发更完整的游戏打下坚实基础。
返回列表