ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

使用Godot 4从零构建Roguelike动作游戏:完整开发流程与实战技巧

使用Godot 4从零构建Roguelike动作游戏:完整开发流程与实战技巧 如果你对独立游戏开发感兴趣尤其是想尝试制作一款类似《土豆兄弟》那样爽快、轻量级的Roguelike动作游戏那么Godot引擎绝对是一个值得深入探索的绝佳选择。相较于Unity或Unreal EngineGodot以其轻量、开源、节点化设计和对2D游戏的深度优化为独立开发者和小团队提供了极高的灵活性和学习友好度。本文将手把手带你从零开始使用Godot 4.x版本一步步构建一个《土豆兄弟》风格的游戏原型。我们将涵盖从项目创建、场景搭建、角色控制、敌人AI、武器系统到Roguelike升级和UI界面的完整流程。无论你是刚接触Godot的新手还是有一定基础想挑战特定类型游戏的开发者这篇教程都将提供一套可直接复用的代码框架和清晰的实现思路。1. 理解《土豆兄弟》风格与Godot 2D开发基础在动手编码之前我们需要明确目标游戏的核心玩法特征并理解Godot引擎处理2D游戏的基本逻辑。1.1 《土豆兄弟》风格解析《土豆兄弟》是一款俯视角、快节奏的Roguelike生存游戏其核心玩法循环可以概括为战斗波次玩家在一波又一波的敌人攻击中生存下来。局内成长每波结束后玩家可以从随机出现的升级选项中如增加伤害、攻速、生命值、获取新武器等选择一项永久强化角色。资源管理通过击败敌人获取金币用于在商店购买道具或升级。构建多样性不同的武器和升级组合能形成截然不同的战斗流派如高射速流、范围伤害流、召唤物流等。从技术实现角度看我们需要实现以下模块一个可移动、可攻击的玩家角色自动生成并具有基础寻路AI的敌人多种可切换或同时生效的武器系统一个处理波次逻辑和升级选择的游戏管理器。1.2 Godot 4 2D开发核心概念Godot采用独特的场景Scene和节点Node系统。一切皆为节点节点通过父子关系组织成树状结构的场景。2D坐标系Godot的2D坐标系原点(0, 0)在屏幕左上角X轴向右为正Y轴向下为正。这对于处理精灵位置和移动至关重要。场景Scene一个可重用的节点集合例如一个“玩家”场景可能包含CharacterBody2D物理身体、Sprite2D图像和CollisionShape2D碰撞形状等节点。节点Node功能的基本单位。对于2D游戏我们最常接触Node2D: 所有2D节点的基类具有位置、旋转、缩放等变换属性。CharacterBody2D: 用于需要复杂移动如平台跳跃、斜坡行走的角色通过代码控制移动和碰撞响应。Area2D: 用于检测区域例如武器的攻击范围、敌人的感知范围、拾取物品的范围。Sprite2D: 用于显示2D纹理图像。CollisionShape2D/CollisionPolygon2D: 为物理身体或区域定义碰撞形状。GDScript: Godot内置的Python-like脚本语言语法简洁与引擎深度集成是本教程使用的脚本语言。2. 环境准备与项目创建2.1 安装Godot引擎访问Godot官网下载页面。根据你的操作系统Windows/macOS/Linux下载最新的稳定版本推荐4.2或更高版本。对于本教程选择标准版本即可。解压下载的压缩包直接运行可执行文件无需安装。2.2 创建新项目打开Godot引擎点击“新建项目”。输入项目名称例如PotatoBrosClone。选择项目存储路径。在“渲染器”选项中对于纯2D项目选择“兼容性”渲染器足以获得最佳性能和兼容性。如果你想使用更现代的特性也可以选择“向前兼容”。点击“创建并编辑”Godot将创建项目并打开编辑器。2.3 初始项目结构设置创建后你的项目面板默认会有一些文件夹。我们手动创建更清晰的结构scenes/存放所有场景文件.tscnplayer/enemies/weapons/ui/world/scripts/存放所有GDScript脚本文件.gdassets/存放所有资源图片、声音、字体等sprites/audio/fonts/你可以在文件系统面板中右键点击res://选择“新建文件夹”来创建它们。3. 创建玩家角色与基础移动3.1 构建玩家场景在“场景”面板中点击“其他节点”。搜索并添加一个CharacterBody2D节点重命名为Player。这将是玩家场景的根节点。为Player节点添加子节点Sprite2D重命名为Visual用于显示玩家图像。暂时可以为其指定一个占位纹理如Godot自带的icon.svg。CollisionShape2D用于物理碰撞。在其Shape属性中新建一个CircleShape2D并调整半径以适应精灵大小。Camera2D重命名为PlayerCamera使其成为玩家的子节点这样相机就会跟随玩家移动。勾选其Enabled属性。你的玩家场景树应类似于Player (CharacterBody2D) ├── Visual (Sprite2D) ├── CollisionShape2D └── PlayerCamera (Camera2D)保存场景到scenes/player/player.tscn。3.2 编写玩家移动脚本选中根节点Player在检查器面板中点击“脚本”旁边的图标选择“新建脚本”。将其保存到scripts/player/player.gd。确保“附加到节点”选项选中。编写移动逻辑。我们将实现八方向移动速度受输入强度影响。# player.gd extends CharacterBody2D export var speed: float 300.0 # 导出变量方便在编辑器中调整 func _physics_process(delta: float) - void: # 获取输入向量 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) # 标准化输入向量以确保斜向移动速度一致但保留输入强度用于可能的动画混合 var direction : input_vector.normalized() # 计算速度 velocity direction * speed # 使用 move_and_slide 移动角色它会自动处理碰撞 move_and_slide() # 可选根据移动方向旋转精灵例如让角色面朝移动方向或鼠标 # _rotate_sprite_towards_mouse()3.3 配置输入映射为了让Input.get_axis工作我们需要在项目设置中定义输入动作。点击顶部菜单项目 - 项目设置。切换到“输入映射”标签页。添加以下动作Actionmove_left 分配按键A和Left Arrow。move_right分配按键D和Right Arrow。move_up分配按键W和Up Arrow。move_down分配按键S和Down Arrow。shoot分配鼠标左键Mouse Button Left为后续攻击做准备。3.4 让玩家朝向鼠标在俯视角游戏中玩家通常朝向鼠标光标。我们在player.gd中添加一个函数来实现。# player.gd (续) func _process(delta: float) - void: _rotate_sprite_towards_mouse() func _rotate_sprite_towards_mouse() - void: # 获取鼠标在游戏世界中的位置 var mouse_pos: Vector2 get_global_mouse_position() # 计算从玩家到鼠标的方向向量 var direction_to_mouse : (mouse_pos - global_position).normalized() # 计算该方向对应的角度弧度并转换为度 var angle_to_mouse : direction_to_mouse.angle() # 旋转根节点或精灵节点。这里旋转整个Player使其面向鼠标。 rotation angle_to_mouse # 如果你只想旋转精灵而不旋转碰撞体可以旋转 $Visual 节点 # $Visual.rotation angle_to_mouse现在运行场景F6你应该可以使用WASD或方向键移动玩家并且玩家角色会始终朝向鼠标光标。4. 实现敌人与基础AI4.1 创建基础敌人场景新建一个场景根节点为CharacterBody2D重命名为BasicEnemy保存为scenes/enemies/basic_enemy.tscn。添加子节点Sprite2D占位纹理、CollisionShape2D圆形。为根节点添加脚本scripts/enemies/basic_enemy.gd。4.2 编写敌人AI脚本追逐玩家敌人需要自动寻找并靠近玩家。我们将使用一个简单的每帧更新位置的逻辑。# basic_enemy.gd extends CharacterBody2D export var speed: float 150.0 export var health: int 30 export var damage: int 10 var player_node: Node2D null func _ready() - void: # 尝试在场景中查找玩家节点。确保你的主场景中玩家节点的名字是“Player”。 # 更好的做法是通过信号或全局单例传递玩家引用这里先使用查找。 player_node get_tree().get_first_node_in_group(player) # 需要将玩家添加到“player”组 if player_node null: print(警告未找到玩家节点。) func _physics_process(delta: float) - void: if player_node null: return # 如果没有玩家不执行移动 # 计算朝向玩家的方向 var direction: Vector2 (player_node.global_position - global_position).normalized() velocity direction * speed move_and_slide() func take_damage(amount: int) - void: health - amount if health 0: die() func die() - void: # 播放死亡动画、音效生成经验球等 queue_free() # 立即从场景中移除4.3 将玩家添加到“player”组为了让敌人能找到玩家我们需要将玩家节点添加到一个组里。打开player.tscn选中根节点Player。在检查器面板中切换到“节点”标签页。在“组”输入框中输入player然后点击“添加”。现在所有敌人都可以通过get_tree().get_first_node_in_group(player)找到这个玩家。5. 构建武器与攻击系统5.1 创建子弹场景新建场景根节点为Area2D重命名为Bullet保存为scenes/weapons/bullet.tscn。添加子节点Sprite2D一个小圆形纹理、CollisionShape2D圆形。添加一个Timer节点重命名为LifetimeTimer用于子弹存活时间。为根节点添加脚本scripts/weapons/bullet.gd。# bullet.gd extends Area2D export var speed: float 800.0 export var damage: int 20 export var lifetime: float 2.0 # 秒后自动消失 var direction: Vector2 Vector2.RIGHT # 发射方向由武器设置 func _ready() - void: # 设置生命周期计时器 $LifetimeTimer.wait_time lifetime $LifetimeTimer.start() # 连接信号 $LifetimeTimer.timeout.connect(_on_lifetime_timer_timeout) body_entered.connect(_on_body_entered) # Area2D进入信号 func _physics_process(delta: float) - void: # 沿着方向移动 global_position direction * speed * delta func _on_body_entered(body: Node) - void: # 如果碰撞到敌人造成伤害 if body.is_in_group(enemies): body.take_damage(damage) # 子弹命中后消失 queue_free() # 也可以添加碰撞到墙壁消失的逻辑 # elif body.is_in_group(walls): # queue_free() func _on_lifetime_timer_timeout() - void: queue_free() # 时间到销毁子弹5.2 创建基础武器脚本附加到玩家我们不在玩家场景中直接写死攻击逻辑而是创建一个可附加的武器脚本。首先在玩家脚本中添加武器管理逻辑。# player.gd (新增部分) export var weapon_scene: PackedScene # 在编辑器中分配子弹场景 var can_shoot: bool true export var fire_rate: float 0.2 # 每秒发射数 var fire_timer: float 0.0 func _process(delta: float) - void: _rotate_sprite_towards_mouse() # 射击冷却处理 if not can_shoot: fire_timer - delta if fire_timer 0: can_shoot true # 检测射击输入 if Input.is_action_pressed(shoot) and can_shoot: shoot() can_shoot false fire_timer 1.0 / fire_rate # 重置冷却计时器 func shoot() - void: if weapon_scene null: return var bullet_instance: Node2D weapon_scene.instantiate() # 设置子弹的初始位置和方向 get_parent().add_child(bullet_instance) # 通常添加到主场景或一个专门的容器中 bullet_instance.global_position $Muzzle.global_position if has_node(Muzzle) else global_position # 计算朝向鼠标的方向 var mouse_pos: Vector2 get_global_mouse_position() var shoot_direction: Vector2 (mouse_pos - global_position).normalized() bullet_instance.direction shoot_direction # 可选播放射击音效注意这里假设玩家节点下有一个Muzzle节点Marker2D类型作为枪口位置。你需要在玩家场景中添加这个节点并放置在精灵前方合适的位置。5.3 配置武器打开player.tscn。添加一个Marker2D节点作为Player的子节点重命名为Muzzle将其放置在精灵前方例如X轴正方向偏移20像素。在检查器中找到Player节点的脚本变量weapon_scene点击下拉框或拖拽将scenes/weapons/bullet.tscn分配给它。调整fire_rate和子弹的speed、damage属性以达到理想的射击手感。6. 实现游戏管理器与波次系统6.1 创建游戏管理器GameManager游戏管理器将作为单例Autoload全局管理游戏状态、波次、分数等。点击顶部菜单项目 - 项目设置 - Autoload。在“路径”中点击文件夹图标新建一个脚本scripts/singletons/game_manager.gd。在“节点名称”中输入GameManager。点击“添加”然后“关闭”。现在GameManager会在游戏启动时自动加载并可在任何脚本中通过GameManager访问。# game_manager.gd extends Node signal wave_started(wave_number: int) signal wave_cleared(wave_number: int) signal game_over var current_wave: int 0 var enemies_in_wave: int 0 var enemies_alive: int 0 var score: int 0 var is_game_active: bool true func start_new_wave() - void: if not is_game_active: return current_wave 1 # 根据波次计算敌人数量和类型这里简化处理 enemies_in_wave 5 current_wave * 2 enemies_alive enemies_in_wave wave_started.emit(current_wave) spawn_enemies() func spawn_enemies() - void: # 这里需要获取一个“生成点”或随机位置。我们假设有一个名为 EnemySpawner 的节点组。 var spawners get_tree().get_nodes_in_group(enemy_spawner) if spawners.is_empty(): print(错误未找到敌人生成点。请将生成点添加到‘enemy_spawner’组。) return for i in range(enemies_in_wave): # 随机选择一个生成点 var spawner: Node2D spawners.pick_random() # 实例化敌人 var enemy_scene preload(res://scenes/enemies/basic_enemy.tscn) var enemy_instance enemy_scene.instantiate() # 将敌人添加到主场景通常是GameManager的父节点或一个专门的容器 get_tree().current_scene.add_child(enemy_instance) enemy_instance.global_position spawner.global_position Vector2(randf_range(-50, 50), randf_range(-50, 50)) # 连接敌人的死亡信号以便计数 # 需要在敌人脚本中定义 died 信号 signal died # enemy_instance.died.connect(_on_enemy_died) func _on_enemy_died() - void: enemies_alive - 1 score 10 # 每杀死一个敌人得10分 if enemies_alive 0: wave_cleared.emit(current_wave) # 波次结束后等待几秒开始下一波或者等待玩家手动触发 # 例如 await get_tree().create_timer(3.0).timeout # start_new_wave() func game_over() - void: is_game_active false game_over.emit() # 可以在这里显示游戏结束UI暂停游戏等6.2 创建主场景与生成点新建一个主场景Main.tscn根节点为Node2D。将保存好的player.tscn实例化拖入主场景。添加几个Marker2D节点作为敌人生成点将它们重命名为Spawner1、Spawner2等并将每个生成点添加到“enemy_spawner”组方法同将玩家添加到“player”组。将Main.tscn设为项目的主场景项目设置 - 应用 - 运行 - 主场景。7. 设计UI生命值、分数与升级选择7.1 创建HUD场景新建场景根节点为CanvasLayer重命名为HUD保存为scenes/ui/hud.tscn。CanvasLayer确保UI始终绘制在最上层。添加控件节点Label重命名为ScoreLabel用于显示分数。Label重命名为WaveLabel用于显示当前波次。ProgressBar或TextureProgressBar重命名为HealthBar用于显示玩家生命值。一个VBoxContainer节点重命名为UpgradeMenu用于在波次间隙显示升级选项。初始将其Visible属性关闭。为HUD根节点添加脚本scripts/ui/hud.gd。# hud.gd extends CanvasLayer onready var score_label: Label $ScoreLabel onready var wave_label: Label $WaveLabel onready var health_bar: ProgressBar $HealthBar onready var upgrade_menu: VBoxContainer $UpgradeMenu func _ready() - void: # 连接GameManager的信号 GameManager.wave_started.connect(_on_wave_started) GameManager.wave_cleared.connect(_on_wave_cleared) GameManager.game_over.connect(_on_game_over) # 初始化显示 update_score(0) update_wave(0) # 假设玩家有一个 health_changed 信号 # Player.health_changed.connect(update_health) func update_score(new_score: int) - void: score_label.text 分数: %d % new_score func update_wave(wave_num: int) - void: wave_label.text 波次: %d % wave_num func update_health(current: float, max_health: float) - void: health_bar.max_value max_health health_bar.value current func _on_wave_started(wave_num: int) - void: update_wave(wave_num) upgrade_menu.hide() # 波次开始隐藏升级菜单 func _on_wave_cleared(wave_num: int) - void: # 波次清除显示升级菜单 show_upgrade_menu() func show_upgrade_menu() - void: upgrade_menu.show() # 这里应该生成3个随机的升级选项按钮。 # 例如清除现有选项然后实例化3个 UpgradeOption 场景设置其文本和效果添加到 upgrade_menu 中。 # 每个按钮被按下时应用升级效果然后隐藏菜单并通知GameManager开始下一波。 func _on_game_over() - void: # 显示游戏结束画面 pass7.2 将HUD添加到主场景打开Main.tscn将hud.tscn实例化拖入其中。8. 整合与运行测试现在我们已经有了玩家、敌人、武器、游戏管理器和UI的核心模块。接下来需要将它们连接起来。完善敌人死亡信号在basic_enemy.gd中添加signal died并在die()函数中发出这个信号。修改GameManager中的_on_enemy_died连接逻辑。玩家生命值在player.gd中添加生命值属性和受伤逻辑并发出health_changed信号供HUD更新。波次控制流程在GameManager中实现波次开始的触发例如游戏开始或上一波结束3秒后并在波次清除后显示升级菜单等待玩家选择后再开始下一波。升级系统创建一个UpgradeOption场景一个按钮代表一种升级如“伤害10%”。在HUD的show_upgrade_menu中动态生成3个随机选项。选择后通过信号或直接调用Player或GameManager的方法来应用升级效果。这是一个简化的启动测试流程确保Main.tscn是主场景。运行游戏F6。你应该能控制角色移动并朝向鼠标。按下鼠标左键应该从玩家位置向鼠标方向发射子弹。你需要手动在GameManager的_ready()函数中调用start_new_wave()来生成第一波敌人。敌人生成后应自动朝玩家移动。子弹击中敌人敌人血量减少血量为零时消失。当所有敌人都被消灭在输出面板查看波次清除信号或后续实现UI菜单。9. 常见问题与排查思路在开发过程中你可能会遇到以下典型问题问题现象可能原因排查思路玩家无法移动1. 输入映射未正确配置。2._physics_process函数未被重写或代码有误。3. 碰撞形状设置过大或位置错误导致move_and_slide被阻挡。1. 检查项目设置中的输入映射确保按键已分配。2. 在_physics_process中添加print(input_vector)调试输入值。3. 在编辑器中检查玩家碰撞形状启用“可见碰撞形状”。子弹不发射或方向错误1.weapon_scene导出变量未分配。2.shoot()函数未被调用检查输入动作和冷却逻辑。3. 子弹实例化后父节点错误导致位置或方向计算基于错误坐标系。1. 在检查器中确认Player节点的weapon_scene已赋值。2. 在shoot()函数开头添加print(Shooting!)进行调试。3. 打印bullet_instance.global_position和shoot_direction检查值。确保子弹被添加到正确的场景层级。敌人不移动或找不到玩家1. 玩家未添加到“player”组。2.get_first_node_in_group返回null。3. 敌人的_physics_process中player_node为null时未提前返回。1. 确认玩家根节点在“组”面板中属于“player”组。2. 在敌人_ready()中打印player_node。3. 使用get_tree().get_nodes_in_group(player)并检查数组长度。游戏运行卡顿随着敌人增多更严重1. 每帧执行的逻辑过于复杂或存在内存泄漏如未正确释放节点。2. 大量物理计算或碰撞检测。1. 使用Godot的性能分析器调试器 - 分析器。2. 确保子弹、敌人在销毁时使用queue_free()。3. 对于大量敌人考虑使用NavigationAgent2D进行性能更好的群体寻路或简化AI逻辑。场景切换或信号连接错误1. 尝试访问的节点路径 ($NodePath) 不存在。2. 信号连接时目标节点还未准备好。1. 使用has_node()检查节点是否存在或使用onready注解延迟获取节点引用。2. 确保在_ready()函数中进行信号连接此时节点树已构建完成。10. 扩展优化与最佳实践完成基础原型后你可以从以下方向扩展游戏并遵循一些Godot开发的最佳实践10.1 游戏性扩展更多敌人类型创建不同的敌人场景如快速型、坦克型、远程攻击型继承自一个基础敌人脚本重写其移动或攻击逻辑。武器系统升级实现多种武器霰弹枪、激光、导弹并设计一个武器切换或同时装备多个武器的系统。完整的升级体系设计一个包含数十种升级的池子分为不同稀有度普通、稀有、史诗并实现一个平衡的随机选择算法。地图与障碍物添加TileMap节点来创建墙壁和障碍物为敌人和子弹添加与墙壁的碰撞逻辑。音效与音乐使用AudioStreamPlayer节点为射击、击中、升级等事件添加音效使用AudioStreamPlayer播放背景音乐。粒子效果使用GPUParticles2D为子弹轨迹、击中爆炸、升级特效等添加视觉反馈。10.2 Godot开发最佳实践使用信号Signals进行松耦合通信避免节点间直接强引用。例如敌人死亡时发出died信号由GameManager监听并更新计数而不是敌人直接调用GameManager的方法。合理使用场景Scene与实例化Instantiate将可重用的对象敌人、子弹、特效制作成场景通过instantiate()动态生成。这比在代码中手动组装节点更高效、更易维护。利用导出变量export进行调试和平衡将速度、伤害、生命值等数值设为导出变量可以在编辑器中实时调整无需修改代码极大方便了游戏平衡性调试。使用组Groups管理对象如上文用“player”和“enemy”组来快速查找和筛选节点这是一种轻量级的管理方式。资源管理将纹理、声音等资源放在assets/目录下规范管理。对于大量使用的资源如子弹场景使用preload()在脚本开头加载避免运行时重复加载。性能考量对于不再需要的节点如飞出屏幕的子弹、死亡的敌人及时调用queue_free()。谨慎使用_process和_physics_process避免在其中进行昂贵的计算。对于大量相似对象如子弹可以考虑使用对象池Object Pooling技术来复用节点减少实例化和垃圾回收的开销。通过本教程你已经掌握了使用Godot 4构建一个《土豆兄弟》风格游戏原型的完整流程。从场景搭建、节点控制、物理与碰撞、UI交互到游戏状态管理这些是2D动作游戏开发的核心。接下来你可以深入每个模块用更丰富的艺术资源、更复杂的AI行为和更庞大的升级系统来填充你的游戏世界。Godot的官方文档和活跃的社区是解决问题的宝贵资源多动手实验享受创造的乐趣。
返回列表