
1. 项目概述与核心价值如果你一直想试试游戏开发但被Unity、Unreal Engine这些庞然大物吓退或者厌倦了它们复杂的安装流程和臃肿的界面那么Godot引擎绝对是你不能错过的选择。作为一个开源、免费、轻量级安装包仅几十MB且功能强大的游戏引擎Godot以其独特的场景-节点架构和易上手的GDScript语言正在成为独立开发者和新手的首选。今天我们就用Godot 4.x来制作一个经典的2D小游戏——《躲避怪物》。这个项目麻雀虽小五脏俱全。你将亲手实现玩家控制、怪物生成、碰撞检测、分数统计和游戏状态管理这些核心机制。整个过程就像搭积木一样直观创建场景、添加节点、编写脚本、连接信号。学完这个项目你不仅能收获一个可玩的游戏更能透彻理解Godot开发2D游戏的完整工作流和核心思想。无论你是编程新手还是从其他引擎转过来的老手这个项目都能帮你快速建立对Godot的“手感”。2. 环境准备与项目初始化2.1 获取并安装Godot引擎首先你需要去Godot官网的下载页面。这里有个小技巧对于新手我强烈推荐下载标准版本Standard version而不是包含.NET支持的Mono版本。标准版本更轻量启动更快完全能满足我们2D游戏的需求。下载完成后你会发现Godot是一个独立的可执行文件无需安装直接双击就能运行。把它放在一个你容易找到的目录比如D:\DevTools\Godot。为了方便你甚至可以创建一个桌面快捷方式。2.2 创建并组织你的第一个项目启动Godot你会看到项目管理器。点击右上角的“新建项目”。项目名称 我习惯用英文这里填DodgeTheCreeps。避免使用中文和空格这能减少未来可能出现的路径问题。项目路径 选择一个干净的文件夹比如D:\GodotProjects\DodgeTheCreeps。Godot会在这个文件夹下创建项目。渲染器 对于2D游戏选择“兼容性Compatibility”渲染器是最稳妥的。它兼容性最好能在绝大多数电脑上运行。等以后做复杂3D项目时再考虑“Forward”或“移动端”渲染器。点击“创建并编辑”Godot会创建项目并打开主编辑器界面。2.3 导入游戏资源素材在动手写代码前我们先准备好“食材”。你需要一些图片和音效。你可以自己用Aseprite、Piskel等像素画工具绘制或者从Kenney.nl等免费资源网站获取。为了教程的连贯性我们假设你已经有了以下资源player.png: 玩家角色精灵图例如一个太空飞船或小英雄。enemy.png: 怪物精灵图例如一个外星人或骷髅头。background.png: 游戏背景图。hit.wav: 碰撞音效。music.ogg: 背景音乐。在Godot编辑器左侧的“文件系统”面板默认在左下角右键点击res://根目录选择“新建文件夹”创建三个文件夹art放图片、audio放音效、scenes放场景。这是一种清晰的项目组织方式。然后将你准备好的player.png和enemy.png拖拽到art文件夹将音频文件拖拽到audio文件夹。Godot会自动导入它们。对于图片Godot的导入设置通常很智能保持默认即可。对于音效如果你希望hit.wav在游戏中不循环可以在导入后在“文件系统”面板中点击该文件在右侧的“导入”面板中将“循环”选项关闭。3. 构建玩家角色场景在Godot中一切皆是场景Scene而场景由节点Node构成。玩家角色本身就是一个独立的、可复用的场景。3.1 创建玩家场景与节点结构在“场景”面板中点击左上角的“”号按钮选择“创建根节点”。因为我们制作的是2D游戏所以根节点选择CharacterBody2D。这个节点是专门为需要自定义移动逻辑如平台跳跃、顶球的2D角色设计的它自带碰撞检测和物理响应但移动完全由我们通过代码控制非常灵活。将根节点重命名为Player。然后我们需要为这个“身体”添加“外观”和“碰撞体”。添加外观Sprite2D 选中Player节点点击顶部的“添加子节点”按钮或按CtrlA搜索并添加一个Sprite2D节点。在右侧的“检查器”面板中点击Texture属性旁边的[空]选择Load然后找到并加载你放在art/player.png的图片。添加碰撞形状CollisionShape2D 再次选中Player节点添加一个CollisionShape2D子节点。这个节点用于定义玩家的物理碰撞边界。在“检查器”中点击Shape属性旁边的[空]选择新建 RectangleShape2D。一个绿色的矩形框会出现在玩家精灵周围。你可以拖动矩形边缘的橙色小点调整它的大小使其大致匹配玩家精灵的轮廓。原则是碰撞体略小于视觉精灵这样玩家感觉上不会被“蹭”到体验更友好。你的玩家场景树现在应该看起来像这样Player (CharacterBody2D) ├── Sprite2D └── CollisionShape2D3.2 编写玩家移动脚本现在让我们赋予玩家生命——用代码控制它移动。选中Player根节点在右侧“检查器”面板最下方点击“添加脚本”按钮。保持默认路径保存到scenes/player.gd语言选择GDScript。GDScript是Godot内置的脚本语言语法类似Python非常易读易写。将以下代码替换到脚本编辑器中extends CharacterBody2D # 定义玩家移动速度像素/秒 export var speed: int 400 func _physics_process(delta: float) - void: # 初始化移动向量为 (0, 0) var direction : Vector2.ZERO # 检测键盘输入 if Input.is_action_pressed(move_right): direction.x 1 if Input.is_action_pressed(move_left): direction.x - 1 if Input.is_action_pressed(move_down): direction.y 1 if Input.is_action_pressed(move_up): direction.y - 1 # 归一化方向向量防止斜向移动更快 if direction.length() 0: direction direction.normalized() # 计算本帧的速度 velocity direction * speed # 调用父类方法进行移动和碰撞检测 # move_and_slide() 会自动处理与墙壁等物体的碰撞 move_and_slide()代码解析与避坑指南export var speed: int 400:export关键字是一个魔法。它将该变量暴露在编辑器的“检查器”面板中。这意味着你之后可以在不修改代码的情况下直接在编辑器里调整玩家速度非常适合调试和平衡游戏。_physics_process(delta): 这是Godot的内置函数每帧调用一次但调用频率与物理帧率同步默认60Hz。所有与物理、移动相关的逻辑都应该放在这里而不是_process里以确保移动的稳定性和确定性。Input.is_action_pressed(): 检测某个“输入动作”是否被持续按下。这里我们用了”move_right”等动作名但还没定义它们。direction.normalized(): 这是关键一步。如果不归一化当玩家同时按住右上键时方向向量是(1, 1)长度约为1.414速度会变成1.414 * 400比只按一个键移动更快。归一化后向量长度始终为1确保八个方向移动速度一致。move_and_slide():CharacterBody2D的核心方法。它根据velocity速度移动角色并在碰撞发生时自动停止。它会返回一个KinematicCollision2D对象我们稍后会用到。3.3 配置输入映射我们提到了”move_right”等输入动作现在来定义它们。点击编辑器顶部菜单的“项目” - “项目设置”。切换到“输入映射”标签页。在“动作”输入框中输入move_right点击“添加”。点击新添加的move_right动作旁边的“”号。在弹出的对话框中选择“键盘”设备然后在物理键位下拉框中选择你想要的键比如D或Right。点击“添加”。重复步骤2-4为move_leftA/Left、move_upW/Up、move_downS/Down添加对应的键盘映射。实操心得 强烈建议为每个动作绑定多个键比如方向键和WASD并考虑未来支持手柄。在“输入映射”中你可以为同一个动作添加多个设备的不同输入Godot会自动识别。现在你可以点击编辑器顶部的“运行当前场景”按钮或按F6来测试玩家移动。你应该能用键盘控制玩家在2D视口中自由移动了。4. 设计怪物敌人怪物需要自动移动并且被玩家碰到后游戏应该结束。我们同样用一个独立的场景来制作它。4.1 创建敌人场景在“场景”面板中点击左上角场景名称旁边的“”选择“新建场景”。这次根节点选择Area2D。Area2D节点非常适合用来做触发区域比如捡道具、进入陷阱、或者像我们这里——检测与玩家的碰撞。将根节点重命名为Mob怪物。然后为其添加子节点Sprite2D: 加载art/enemy.png作为纹理。CollisionShape2D: 为其新建一个CircleShape2D或RectangleShape2D调整大小匹配精灵。场景树如下Mob (Area2D) ├── Sprite2D └── CollisionShape2D4.2 编写敌人行为脚本为Mob根节点添加脚本保存为scenes/mob.gd。extends Area2D # 导出最小和最大速度方便在编辑器中调整 export var min_speed: float 150.0 export var max_speed: float 250.0 func _ready() - void: # 为这个怪物实例随机选择一个速度 var mob_speed randf_range(min_speed, max_speed) # 计算一个随机的移动方向角度 var direction randf_range(0, TAU) # TAU 等于 2 * PI是一个完整的弧度圆 # 根据角度和速度计算速度向量 velocity Vector2.from_angle(direction) * mob_speed func _physics_process(delta: float) - void: # 每帧根据速度移动位置 position velocity * delta # 当怪物离开屏幕时自动删除自己以释放资源 func _on_visible_on_screen_notifier_2d_screen_exited() - void: queue_free()代码解析与注意事项_ready(): 节点进入场景树时调用一次用于初始化。这里我们为每个生成的怪物随机设定速度和方向。randf_range(): Godot内置的随机数函数生成指定范围内的随机浮点数。TAU是数学常量代表2 * PI即360度对应的弧度值。Vector2.from_angle(): 非常实用的函数根据给定的弧度角返回一个单位向量。queue_free(): 安全地删除节点及其所有子节点。重要在Godot中不要直接使用remove_child()或free()queue_free()会在当前帧的安全时机执行删除避免在物理或渲染过程中出现错误。4.3 添加屏幕退出检测为了让怪物离开屏幕后自动消失我们需要一个“哨兵”。选中Mob节点添加一个VisibleOnScreenNotifier2D子节点。这个节点会在其父节点怪物完全离开屏幕视口时发出screen_exited信号。选中新添加的VisibleOnScreenNotifier2D节点。在右侧“检查器”面板上方的“节点”标签页切换到“信号”子标签。找到screen_exited()信号双击它。在弹出的连接信号窗口中目标节点选择Mob自身方法名保持为_on_visible_on_screen_notifier_2d_screen_exited这是Godot自动生成的推荐名称与我们在脚本中写的函数名一致。点击“连接”。这样当怪物移出屏幕信号就会触发调用我们脚本中的函数将其删除。5. 构建游戏主场景与逻辑主场景是游戏的“导演”负责生成怪物、管理游戏状态开始、进行中、结束、计算分数等。5.1 搭建主场景框架新建一个场景根节点选择Node2D命名为Main。保存为scenes/main.tscn。我们需要以下节点Player实例 在“文件系统”面板中找到你之前保存的player.tscn直接拖拽到Main场景的视口中。Godot会询问你是否实例化选择“实例化子场景”。将其放置在屏幕中央偏下的位置。怪物生成路径Path2D 添加一个Path2D节点。这个节点将定义怪物出现的路径。在2D视口中使用顶部工具栏的“钢笔工具”或“曲线编辑工具”在屏幕外侧画一条闭合或开放的曲线。例如画一个围绕屏幕四周的矩形路径但确保路径在屏幕可视区域之外。路径跟随点PathFollow2D 选中Path2D添加一个PathFollow2D子节点。这个节点可以沿着父路径移动。我们将在这个节点的位置生成怪物。计时器Timer 添加一个Timer节点重命名为MobTimer。它将定期触发用于生成怪物。分数标签Label 添加一个Label节点重命名为ScoreLabel。将其放置在屏幕左上角。在“检查器”中调整其“文本”为Score: 0并可以调整字体大小和颜色。开始按钮Button 添加一个Button节点重命名为StartButton。将其放置在屏幕中央。调整其文本为Start Game。消息标签Label 添加另一个Label节点重命名为MessageLabel。将其放置在屏幕中央偏上调整文本为Dodge the Creeps!字体可以调大一些作为标题。你的主场景树结构应类似于Main (Node2D) ├── Player (实例化的 player.tscn) ├── Path2D │ └── PathFollow2D ├── MobTimer (Timer) ├── ScoreLabel (Label) ├── StartButton (Button) └── MessageLabel (Label)5.2 编写主场景控制脚本为Main根节点添加脚本保存为scenes/main.gd。extends Node2D # 预加载怪物场景这样我们可以动态创建它 onready var mob_scene preload(res://scenes/mob.tscn) # 分数变量 var score: int 0 func _ready() - void: # 初始时隐藏玩家暂停生成计时器显示开始按钮和标题 $Player.hide() $MobTimer.stop() $MessageLabel.show() $StartButton.show() func game_start() - void: # 游戏开始重置分数显示玩家隐藏按钮和标题启动计时器 score 0 $ScoreLabel.text Score: %s % score $MessageLabel.hide() $StartButton.hide() $Player.show() $Player.position Vector2(400, 300) # 将玩家重置到初始位置根据你的屏幕大小调整 # 启动怪物生成计时器 $MobTimer.start() func _on_mob_timer_timeout() - void: # 计时器超时生成一个新的怪物 # 1. 实例化怪物场景 var mob mob_scene.instantiate() # 2. 设置怪物的生成位置 var mob_spawn_location $Path2D/PathFollow2D # 让路径跟随点在路径上随机前进一个偏移量 mob_spawn_location.progress_ratio randf() # 将怪物的位置设置为路径跟随点的全局位置 mob.position mob_spawn_location.global_position # 3. 将怪物添加到主场景中 add_child(mob) # 4. 连接怪物的 area_entered 信号用于检测与玩家的碰撞 mob.area_entered.connect(_on_mob_area_entered.bind(mob)) func _on_mob_area_entered(area: Area2D, mob_instance: Area2D) - void: # 当怪物Area2D与玩家也是Area2D等一下玩家是CharacterBody2D发生区域重叠时... # 实际上我们需要检测玩家与怪物的碰撞。更标准的做法是 # 在玩家场景中也添加一个 Area2D 节点来检测与怪物的碰撞。 # 但为了简化我们这里假设主场景能收到信号。 # 一个更健壮的方法是让玩家的碰撞体CollisionShape2D所在的CharacterBody2D与怪物的Area2D碰撞。 # 我们需要修改玩家场景为其添加一个 Area2D 子节点来专门检测与怪物的碰撞。 # 让我们先注释掉这部分稍后完善。 # game_over() pass # 完善后的碰撞检测函数假设玩家场景有一个名为Hitbox的Area2D func _on_player_hit() - void: game_over() func game_over() - void: # 游戏结束停止生成怪物显示按钮和标题隐藏玩家 $MobTimer.stop() $MessageLabel.text Game Over! $MessageLabel.show() $StartButton.show() $Player.hide() # 清除所有现有的怪物 get_tree().call_group(mobs, queue_free) func _on_start_button_pressed() - void: # 开始按钮被按下启动游戏 game_start()代码深度解析与优化onready var mob_scene preload(...):onready关键字确保该变量在节点_ready()函数调用之前就被赋值。preload()在加载场景时就将资源读入内存运行时instantiate()实例化速度更快。对于频繁生成的对象这是最佳实践。$NodeName: 这是get_node(“NodeName”)的简写用于快速获取场景树中的子节点。注意如果节点路径改变或节点被移除使用$的代码会报错。对于复杂的场景更推荐使用onready var node $NodeName在_ready中提前获取引用。progress_ratio:PathFollow2D的属性范围0到1表示在路径上的进度。设置为随机值让怪物在路径上随机位置出现。area_entered.connect(...): 动态连接信号。当怪物实例被创建后我们将其area_entered信号连接到主场景的一个函数。.bind(mob_instance)将怪物实例本身作为额外参数传递方便后续操作例如知道是哪个怪物撞到了玩家。get_tree().call_group(“mobs”, “queue_free”):get_tree()获取场景树。我们可以在生成怪物时将其加入一个名为”mobs”的组mob.add_to_group(“mobs”)这样在游戏结束时可以方便地一次性删除所有怪物。5.3 完善碰撞检测机制上面代码中碰撞检测部分留了空。我们需要一个更可靠的方案。回到player.tscn场景选中Player根节点 (CharacterBody2D)。添加一个Area2D子节点重命名为Hitbox。为Hitbox添加一个CollisionShape2D子节点形状可以设置得比玩家的碰撞体稍大一点这样“碰到”的感觉更明显。为Hitbox节点添加脚本或直接在Player脚本中处理。这里我们选择为Hitbox添加一个简短的脚本hitbox.gdextends Area2D # 当有物体进入此区域时 func _on_area_entered(area: Area2D) - void: # 假设进入的area是怪物 # 发出一个自定义信号通知主场景玩家被击中 emit_signal(hit) # 在编辑器中定义自定义信号 signal hit在Player场景中选中Hitbox节点在“节点”标签的“信号”页将area_entered信号连接到Hitbox节点自身的_on_area_entered方法。回到main.tscn场景选中实例化的Player节点在“节点”标签的“信号”页你应该能看到Hitbox节点发出的hit信号。将其连接到Main节点的_on_player_hit方法你需要先在main.gd中创建这个方法。现在当怪物的Area2D与玩家的Hitbox也是一个Area2D重叠时就会触发hit信号主场景的_on_player_hit被调用游戏结束。5.4 实现分数系统我们需要在玩家存活时分数随时间增加。修改main.gd添加一个Timer节点到Main场景重命名为ScoreTimer设置其Wait Time等待时间为1秒并勾选Autostart自动启动为关闭。在game_start()函数中启动这个计时器$ScoreTimer.start()。在game_over()函数中停止这个计时器$ScoreTimer.stop()。为ScoreTimer的timeout信号创建一个新的函数func _on_score_timer_timeout() - void: score 1 $ScoreLabel.text Score: %s % score连接ScoreTimer的timeout信号到Main节点的_on_score_timer_timeout方法。6. 打磨与发布6.1 添加音效和背景音乐好的音效能极大提升游戏体验。背景音乐 在Main场景中添加一个AudioStreamPlayer节点重命名为MusicPlayer。在“检查器”中将其Stream属性设置为你的music.ogg文件。在game_start()函数中调用$MusicPlayer.play()在game_over()中调用$MusicPlayer.stop()。碰撞音效 在Player场景中为Hitbox节点添加一个AudioStreamPlayer子节点重命名为HitSound并加载hit.wav。在Hitbox脚本的_on_area_entered函数中添加$HitSound.play()。6.2 优化怪物生成目前的怪物生成是纯随机的可能导致怪物堆在一起。我们可以增加一些规则在_on_mob_timer_timeout中生成怪物前检查新怪物的位置是否与现有玩家或其他怪物太近如果太近就重新随机一个位置。动态调整生成速率随着分数增加逐渐缩短MobTimer的wait_time让游戏越来越难。6.3 游戏发布当你对游戏满意后就可以将其分享给朋友了。点击编辑器顶部菜单的“项目” - “导出…”。点击“添加…”选择目标平台例如 “Windows Desktop”。在右侧的导出设置中你可以配置应用图标、文件名称等。最关键的一步点击“导出项目”下方的“管理导出模板…”然后下载对应平台的导出模板。Godot需要这些模板来构建最终的可执行文件。下载好模板后回到导出窗口选择好导出路径点击“导出项目”。Godot会生成一个独立的.exeWindows或.appmacOS文件以及一个.pck数据文件。将它们放在一起就可以分发了。避坑指南 首次导出可能会遇到“未找到导出模板”的错误。务必通过编辑器内置的下载器获取模板这比手动下载更可靠。如果导出后游戏运行黑屏或报错检查“文件系统”面板中是否有资源被错误引用显示为红色或者是否漏掉了某些关键场景。7. 常见问题与进阶思路7.1 我按了按键但玩家不动检查输入映射 确保在“项目设置 - 输入映射”中动作名如move_right与代码中Input.is_action_pressed里写的完全一致包括大小写。检查脚本连接 确保脚本正确附加到了Player节点并且_physics_process函数名拼写正确。检查节点类型 玩家根节点必须是CharacterBody2D或RigidBody2DSprite2D和CollisionShape2D是其子节点。7.2 怪物生成后不动或者乱飞检查速度计算 确保在mob.gd的_ready函数中velocity被正确计算和赋值。velocity应该是一个Vector2类型。检查_physics_process 确保该函数被正确定义并且内部有position velocity * delta这行代码。检查坐标系统 确认怪物的移动方向符合预期。你可以打印velocity的值来调试。7.3 碰撞检测不生效检查碰撞层和遮罩 在“检查器”面板中Area2D和CollisionShape2D节点都有“碰撞”区域。确保玩家的HitboxArea2D和怪物的MobArea2D至少有一层碰撞层/遮罩是重叠的。默认情况下它们都在第1层所以应该能碰撞。检查信号连接 在场景编辑器中选中相关节点查看“节点”标签下的“信号”连接列表确认area_entered或body_entered信号已正确连接到目标函数。使用调试工具 运行游戏时点击编辑器顶部菜单的“调试” - “可见碰撞形状”可以在游戏中看到碰撞体的轮廓非常有助于调试。7.4 游戏感觉卡顿怪物没有及时销毁 确保VisibleOnScreenNotifier2D的screen_exited信号正确连接并且queue_free()被调用。积累大量未销毁的怪物会导致性能下降。优化资源 确保使用的图片尺寸合理不要过大。对于像素风游戏图片缩放使用“最近邻”过滤模式在导入设置中可以保持清晰且性能更好。使用性能分析器 点击编辑器左下角的“调试器”面板切换到“分析器”标签运行游戏可以查看CPU、GPU、内存等占用情况定位性能瓶颈。7.5 接下来可以做什么这个《躲避怪物》项目是一个完美的起点。掌握了这些基础后你可以尝试以下扩展让游戏更具个性和深度增加怪物种类 创建不同的Mob场景比如快慢不同的怪物在主场景的生成逻辑中随机选择。添加道具系统 创建一个PowerUp场景也是Area2D玩家触碰后可以获得临时加速、无敌盾、清屏炸弹等效果。实现关卡系统 将Main场景做得更通用通过读取外部数据或配置改变怪物生成波次、速度、类型形成多个关卡。加入粒子特效 当玩家被击中或怪物被消灭时使用GPUParticles2D节点创建爆炸、闪烁等特效。完善UI和菜单 使用Godot强大的UI系统Control节点制作一个主菜单、暂停菜单、游戏结束界面并保存最高分记录。Godot的魅力在于其简洁和高效。通过这个项目你已经走完了从零到一发布一个完整2D游戏的全流程。核心的节点操作、脚本编写、信号通信、资源管理你都已亲身实践。记住Godot的官方文档和活跃的社区是你最好的老师。多读文档多看社区分享的示例项目你很快就能从“躲避怪物”进化到创造属于你自己的独特世界。