第五章:入门项目实战Flappy Bird
本章导言
在前几章中,你已经掌握了Godot引擎的基本操作和GDScript语言的使用方法,也了解了2D游戏开发所需的核心概念。现在,是时候动手实践一个完整的小游戏项目了!
本章将通过复刻一款经典的2D游戏《Flappy Bird》,带你走完整个2D游戏的开发流程。你将从零开始创建项目,导入资源,搭建游戏场景,实现角色控制、碰撞检测、动画效果、粒子特效、用户界面、音效管理、分数统计和全局信号等功能模块。
通过本章的学习,你将学会:
- 如何将一个游戏拆解为多个子场景,并各自开发与组合;
- 如何使用物理节点处理角色移动与碰撞;
- 如何为游戏角色添加动画与粒子效果;
- 如何构建UI界面,并通过信号机制与游戏逻辑联动;
- 如何管理全局状态与事件,实现游戏开始、得分、结束的完整流程。
本章不仅是你进入实际游戏开发的第一步,也将帮助你建立起系统的开发思维,让你从“知道怎么写代码”迈向“知道怎么做游戏”。准备好了吗?让我们从复刻《Flappy Bird》开始,真正走进Godot 2D游戏世界!
1. 游戏概述与玩法分析
《Flappy Bird》是由越南独立开发者 Dong Nguyen 于 2013 年发布的一款现象级休闲小游戏,中文常译为“笨鸟先飞”。尽管它的画面采用了复古的像素风格,结构也看似简单,但其凭借极具挑战性的物理反馈和“易上手、难精通”的机制,在发布后迅速走红全球,甚至成为了移动时代独立游戏设计的教科书级范例。在本节中,我们将从开发者的专业视角出发,深度拆解这款游戏的玩法逻辑,并梳理出在复刻过程中需要攻克的关键技术模块。
1.1 玩法逻辑
从玩家体验来看,《Flappy Bird》的控制方式被简化到了极致:玩家只需要进行单一的点击动作(在电脑端通常为点击鼠标或敲击空格键)。每点击一次,小鸟就会获得一个向上的瞬时冲力;而一旦停止操作,小鸟便会在重力的模拟下迅速坠落。这种对升降高度的精确把控,构成了游戏唯一的、也是最难的操作点。
游戏的目标非常明确,即控制小鸟穿过由一对对绿色水管组成的狭窄通道,每成功穿越一组障碍,得分便会增加。然而,极低的容错率是其上瘾的核心所在,一旦小鸟的碰撞盒接触到管道边缘或者掉落地面,游戏会立即宣告失败。这种“即时反馈、直接惩罚”的设计,配合极短的游戏循环,让玩家在产生挫败感的同时,会下意识地产生“再试一次就能过”的心理暗示,从而形成高度上瘾的游玩闭环。
1.2 核心机制拆解
从程序开发的角度分析,要复刻这款经典游戏,我们需要将看似整体的画面拆解为数个精密协作的底层机制。首先是玩家控制与重力模拟,这要求我们利用 Godot 的物理节点,为小鸟施加持续的向下加速度,并监听玩家的输入事件来改变其垂直向上的速度向量。为了保证手感丝滑,我们还需要对飞行的角速度和动画进行微调,让小鸟在上升和下落时呈现出逼真的仰俯姿态。
其次是障碍物的生成与移动逻辑。由于游戏场景在视觉上是无限延伸的,开发者通常不会创建一个巨大的关卡,而是利用定时生成器,在屏幕右侧不断产出成对的管道,并让它们以恒定速度向左平移。在这个过程中,碰撞检测起着至关重要的作用:系统需要实时监测小鸟与管道及地面之间的重叠状态。一旦触发碰撞,系统将立即冻结所有节点的移动。
最后是得分系统与状态管理。当小鸟安全通过管道中间的空隙时,我们会设置一个不可见的触发区域,用于捕捉得分事件并更新 UI 界面。这一切都需要通过 Godot 强大的信号(Signal)系统来协调。从游戏首页的点击开始,到进行中的实时交互,再到失败后的结算画面,我们需要建立一套状态管理系统,通过信号在各个子场景之间传递指令,确保游戏的生命周期管理逻辑清晰且稳定。
2. 创建项目与导入资源
在本节中,我们将从零开始创建 Flappy Bird 游戏项目,并导入开发过程中所需的资源文件,为后续开发打下基础。
2.1 创建新项目
打开 Godot 编辑器,按照以下步骤创建一个新的项目:
- 点击主界面的New Project按键新建项目。
- 项目名称输入
Flappy_Bird。 - 选择一个你熟悉的目录作为项目保存路径。
- 渲染器选择默认的
Forward+(适合大多数 2D/3D 项目)。 - 点击“Create & Edit”进入项目。
此时,你已经拥有一个空白项目环境,下一步将对其进行窗口设置与平台适配调整。
2.2 设置窗口尺寸与输入映射
为了模拟手机竖屏的游戏效果,我们需要修改项目的窗口配置。依次点击顶部菜单栏:
Project -> Project Settings -> Display -> Window
在右侧配置面板中进行如下设置:
| 设置项 | 值 | 说明 |
|---|---|---|
Viewport Width |
600 |
游戏画面的宽度,单位为像素 |
Viewport Height |
800 |
游戏画面的高度,模拟竖屏比例 |
Stretch Mode |
canvas_items |
启用自动适配,缩放所有 2D 元素以填充窗口 |
Stretch Aspect |
keep |
保持画面比例,避免拉伸变形 |
Orientation |
portrait |
设置为竖屏方向,适配移动设备; |
我们还需要告诉 Godot 引擎:当玩家按下哪个按键时,程序应该识别为“飞行(fly)”动作。按照以下步骤配置:
- 打开设置:点击菜单栏的 Project(项目)-> Project Settings(项目设置)。
- 切换选项卡:在弹出的窗口顶部,点击 Input Map(输入映射) 标签页。
- 创建动作:在 Add New Action(添加新动作) 的输入框中输入
fly,然后点击 Add(添加)。 - 绑定按键:
- 点击
fly动作右侧的 “+” 号。 - 在弹出的监听窗口中,按下键盘上的 Space(空格键),点击 OK。
- 再次点击 “+” 号,切换到 Mouse Buttons(鼠标按钮),选择 Left Button(左键),点击 OK。
- 完成:现在,你的
fly动作已经关联了空格和鼠标左键。
设置完成后,关闭设置窗口,Godot 会自动保存。
2.3 下载与导入资源文件
为了方便大家快速进入开发状态,本项目所需的所有素材资源(图像、音效和字体等)已经统一打包。你可以从本书配套的 GitHub 仓库中,下载对应章节的 assets.zip 压缩包。以下是资源清单及其在游戏中的具体用途:
图片资源:构建游戏的视觉世界
这些图像构成了游戏的各种元素,从静态背景到多帧动画:
环境背景:
bg.png:一张静态的天空背景图,为游戏提供基础的视觉底色。ground.png:地面的图像,后续我们将通过代码让它循环滚动,营造小鸟在前进的假象。
障碍物:
pipe.png:经典的绿色管道图像,我们将利用它来创建成对出现的上下障碍物。
角色与交互动画:
bird1.png,bird2.png,bird3.png:这一组三张图片构成了小鸟的序列帧动画。通过在 Godot 中快速切换显示这些图像,可以实现小鸟扇动翅膀的动态效果。coin-frame-1.png~coin-frame-11.png:这 11 张图片记录了金币旋转和闪烁的每一个细节,用于增加游戏通过时的视觉反馈。
音效资源:赋予游戏生命力
声音是提升游戏沉浸感和反馈感的关键,本项目包含以下音频文件:
动作音效:
wing.wav:每当玩家点击屏幕让小鸟奋力向上飞跃时,会播放这一短促的扑翼声。
状态反馈音效:
point.wav:当小鸟成功钻过管道间隙并得分时,播放这一清脆的奖励音,给玩家正向激励。hit.wav:当悲剧发生,小鸟撞击障碍物或坠地时,用于提示游戏结束的死亡音效。
氛围音乐:
BGMUSIC.mp3:整款游戏的背景音乐,用于在主场景运行期间循环播放。
字体与样式:美化 UI 界面
为了让显示的分数和菜单更具“游戏感”,我们使用了专门的排版资源:
字体文件:
LuckiestGuy-Regular.ttf:一款极具卡通风格的开源字体,非常适合《Flappy Bird》这类休闲游戏的视觉基调。
Godot 样式资源:
white_32.tres:这是 Godot 引擎特有的资源文件(Resource)。它不仅关联了上述字体,还预设好了字号(32号)、颜色、以及阴影描边等参数。这样我们在制作分数显示、重新开始按钮时,只需直接引用这个文件,就能保证全局 UI 风格的高度统一,极大提高了开发效率。
2.4 导入资源到项目
当准备好所有的原始素材后,我们需要将它们正式引入 Godot 的工作环境。导入资源的操作非常直观,你只需将解压后的整个资源文件夹(例如 assets/)从系统的文件管理器中选中,直接拖拽到 Godot 编辑器左下角的 FileSystem(文件系统)面板 中即可。此时,Godot 会启动后台预处理机制,自动识别图片、音频和字体等不同格式,并为它们生成相应的导入配置。如果你更习惯传统的操作方式,也可以在文件系统面板中点击右键,选择 Open in File Manager(在文件管理器中打开),直接将素材复制粘贴到项目的根目录文件夹下,引擎同样能实时检测并完成刷新。
在资源载入完成后,建议你进行一次简单的“健康检查”以确保后续开发顺畅:首先,点击图片资源,观察编辑器右侧的属性栏或缩略图,确认图像是否可以正常预览,没有出现破损或缺失;其次,双击音效文件,通过检查器面板中的播放按钮试听,确认音频是否能清晰播放;最后,点击字体文件,确认其在编辑器中被识别为可用的字体资源。如果在此过程中发现资源导入失败,请务必检查文件路径是否包含特殊非法字符,或者尝试右键点击该资源并选择“Reimport(重新导入)”来解决潜在的解析错误。
3. 游戏功能模块划分
在开发游戏时,一个重要的工程思想是“分而治之”。即便是看似简单的《Flappy Bird》,其背后也由多个相互协作的功能模块构成。通过将复杂的游戏系统拆分为多个独立子系统,我们可以让开发流程更清晰、调试更方便、协作更高效。
下面我们一起分析 Flappy Bird 游戏中各个功能模块的职责,并在 Godot 引擎中为它们设计对应的场景结构。

3.1 模块划分原则
在进入实际的代码编写之前,设计一个合理的架构至关重要。就像建筑师在动工前需要图纸一样,遵循良好的模块划分原则可以避免后期代码变成一团乱麻。我们在设计本项目的游戏模块时,主要遵循以下三个核心原则:
- 功能单一:每个模块或场景应该只专注于解决一类具体任务。例如,小鸟模块只负责处理自身的物理飞行和碰撞,而不需要关心分数是如何显示在屏幕上的。职责清晰的模块更容易编写,也更方便后期调整。
- 结构独立:模块之间应尽量保持“松耦合”状态。这意味着每一个功能块都应该像一块独立的乐高积木,即使暂时拔掉它,其他部分也不会直接崩溃。
- 层次清晰:我们将整个项目按照逻辑深度划分为不同的层次。底层是基础对象层(如小鸟、地面、管道等);中层是逻辑控制层(如游戏主场景,负责协调各物体的协作);顶层则是界面与管理层(如 UI 面板、全局状态控制)。这种分层结构让开发者能够像“剥洋葱”一样管理项目,当 UI 需要更换皮肤时,底层的物理代码完全不受影响。
3.2 Flappy Bird 模块设计
基于上述的模块划分原则,我们将《Flappy Bird》拆解为五个核心功能模块。在 Godot 中,每一个模块都将以独立场景(Scene)的形式存在。这种“模块化”的设计方式,能让我们像搭建积木一样构建游戏,极大地降低了调试的难度。
以下是具体的模块设计方案:
- 主角模块: 这是游戏的灵魂所在。该模块采用功能单一原则,专注于处理小鸟个体的所有逻辑。它包含了小鸟的物理运动属性(如重力、上升速度)、煽动翅膀的序列帧动画、以及与障碍物接触时的碰撞检测。为了增强表现力,我们还将音效触发(如扑翼声)和可能的死亡粒子特效也封装在这里,使其成为一个具备完整行为能力的独立个体。
- 障碍模块: 该模块是游戏主要的挑战来源。它不仅仅是一对管道的图像,更是一个复合型功能块。其内部包含上下两根带有碰撞形状的管道,以及中心位置一个不可见的“得分触发区”。它的核心职责是接收生成指令后,以恒定的速度向左移动,并在移出屏幕后自动销毁以节省内存。
- 环境模块 : 为了营造出小鸟在广阔世界中持续飞行的错觉,我们将环境分为天空和地面两个部分。地面模块(Ground)具有特殊的职责,它需要与物理引擎交互,作为小鸟坠落的“地板”;而天空背景则更多承担视觉表现。通过赋予它们不同的移动速度,我们可以实现“视差滚动”效果,提升画面的空间感。
- 用户界面模块: 该模块属于展示层,负责所有非游戏世界的元素。它的职责非常纯粹:将内存中的数字(如当前分数、最高记录)转化为屏幕上漂亮的文字,并提供开始游戏或重新开始的交互按钮。它通过信号监听全局状态的变化,并即时更新 UI 界面,确保玩家始终掌握游戏进度。
- 主场景模块: 这是游戏的“大脑”和容器,负责执行层次清晰原则中的最高层管理。它本身不处理具体的物理细节,而是负责整合上述所有子场景。主场景就像一个舞台监督,它决定了游戏何时开始、何时生成新的管道,并监听各个子模块传来的信号,从而切换整个游戏的运行状态。
全局枢纽:GameManager (Autoload)
为了让这些独立的模块能够顺畅通信,我们还引入了 Godot 的单例机制(Autoload)。Autoload 是一个在游戏启动时自动加载、在整个生命周期内始终存在、且能被任何脚本随时访问的“超级节点”。我们会创建一个名为 GameManager 的脚本。它就像一个“中转站”,存储着全局的分数变量,并定义了一系列全局信号。当小鸟穿过管道时,小鸟只需向 GameManager 发送一个“得分”信号,而 HUD 模块会自动接收这个信号并更新数字。这种设计完美实现了模块间的松耦合,各个场景不需要知道彼此的存在,只需要通过 GameManager 进行对话。
3.3 模块协作关系示意图
为了让大家更直观地理解各模块是如何“对话”的,我们可以参考下面的协作关系图。在 Godot 中,场景之间的组合通常是树状结构(实线表示),而它们之间的通信则主要依靠信号机制(虚线表示)。
graph TB
%% 顶层控制
GM[GameManager<br>(全局数据和信号控制)]
%% 主场景控制中心
Main[Main.tscn<br>(主场景)]
%% 功能模块
Bird[Bird.tscn<br>小鸟控制]
Pipes[Pipes.tscn<br>障碍+金币]
HUD[HUD.tscn<br>用户界面]
BG[Sky / Ground<br>动态背景]
%% 场景组合(实线)
Main --> Bird
Main --> Pipes
Main --> HUD
Main --> BG
%% 信号连接(虚线)
GM -.->|信号| Bird
GM -.->|信号| Pipes
GM -.->|信号| HUD
通过上述模型,我们可以清晰地看到不同职责层级之间的交互方式:
-
层级包含(实线): Main 场景作为容器,负责管理所有子场景的生命周期。这种结构保证了当我们删除或替换某个模块时,不会破坏其他模块的运行。
-
信号驱动(虚线): 各模块之间保持“互不相识”的优雅距离。例如,小鸟并不知道 HUD 的存在,它只管发出一个“我撞了”的信号;Main 监听到这个信号后,再通知其他模块停止运行。这种松耦合的设计让代码极易维护。
-
全局中转(GM): 像得分这种跨模块的数据,我们通过 GameManager 进行中转。这样即使当前管道被销毁了,分数依然安全地存储在全局变量中,并能实时同步给 UI 显示。
通过这样的模块拆分与协作设计,我们将原本杂乱的游戏逻辑划分为多个独立的“标准构件”。这种“高内聚、低耦合”的结构,不仅能帮助你更轻松地完成《Flappy Bird》的复刻,更是你未来开发大型复杂游戏项目的基石。
4. 游戏场景实现
在前一节中,我们将 Flappy Bird 游戏拆解成了多个功能模块。每个模块将使用 Godot 的一个场景(Scene)来单独构建与测试,最终由主场景统一组合运行。这种“模块化 + 场景复用”的开发方式,是 Godot 引擎的一大优势,也符合现代游戏开发的工程实践。在本节中,我们将从主角小鸟的场景开始,依次完成各子场景的搭建与实现,逐步构建起整个游戏的功能骨架。
4.1 构建主角场景
作为游戏绝对的主角,小鸟不仅是玩家交互的唯一媒介,它的飞行反馈、视觉动画以及碰撞逻辑更是决定游戏“手感”的核心要素。在本节中,我们将从零开始构建一个功能完备的独立场景 bird.tscn。这个场景将不再是一个简单的静态贴图,而是一个集成了物理反馈、视觉特效和声音系统的对象。
为了赋予这只像素小鸟真实的“生命力”,我们将实现一套完整的行为逻辑。首先,我们会利用 Godot 的物理引擎赋予小鸟受重力影响的特性,并编写代码让它能实时响应玩家的点击指令,产生向上跃起的飞行冲力。在视觉表现上,我们不仅会通过三帧序列图实现流畅的煽动翅膀动画,还会引入粒子特效(Particle Systems)来增强其运动时的动态表现,让飞行轨迹更具冲击力。
此外,声音和碰撞系统也是不可或缺的部分。我们将为小鸟配置专属的音效触发机制,使其在飞行和得分时播放相应的音频回馈。在物理层面,我们会为其设置碰撞检测形状(Collision Shape),确保每一次“惊险穿梭”或“遗憾撞击”都能被引擎准确捕捉。最后,通过 Godot 的分组机制(Groups),我们会为该场景打上“小鸟”的标签,这种设计可以让场景中的其他节点(如管道或地面)在复杂的碰撞发生时,能够瞬间识别出互动对象,从而触发正确的游戏逻辑。
4.1.1 创建场景根节点
首先,我们需要为小鸟选择一个合适的“身体”。新建一个场景,在左侧的创建根节点选项中点击 Other Node(其他节点)。在弹出的搜索对话框中输入 CharacterBody2D 并确认创建。最后,双击该节点将其重命名为 Bird。
为什么选择 CharacterBody2D? 在 Godot 中,它是专门为玩家角色设计的节点类型。它不仅能参与物理碰撞,还内置了处理移动与碰撞的专业方法,非常适合我们需要手动控制跳跃和受重力驱动的游戏主角。
4.1.2 添加碰撞检测区域
当你创建好 Bird 节点后,会发现节点旁边出现了一个黄色警告图标。这是因为物理节点必须定义自己的形状,引擎才能知道它在哪里会发生碰撞。
- 添加子节点: 选中根节点
Bird,按下快捷键Ctrl + A或点击添加子节点按钮,搜索并添加一个CollisionShape2D。 - 分配形状: 在右侧的属性检查器(Inspector)中找到Shape 属性,点击下拉菜单选择 New CircleShape2D(新建圆形形状)。
- 调整尺寸: 将圆形的 Radius(半径) 设置为 17。这样我们就为小鸟构建了一个圆形的“防护罩”,它将决定小鸟在何时会撞击到管道或地面。
4.1.3 设置翅膀动画效果
有了骨架和碰撞区,现在我们需要给小鸟穿上“衣服”并让它动起来。
- 引入动画节点: 再次为
Bird根节点添加一个新的子节点,搜索并选择AnimatedSprite2D。这个节点专门用于处理由多张图片组成的序列帧动画。 - 创建动画帧资源: 在右侧属性检查器中找到 Sprite Frames 选项,点击下拉并选择 New SpriteFrames(新建精灵帧)。
- 导入素材: 点击刚才新建的资源,屏幕底部会自动弹出动画资源编辑器面板。确保左侧选中的动画名称为 default,然后从文件系统中将
bird1.png、bird2.png和bird3.png这三张素材依次拖入右侧的帧列表中。 - 开启循环播放: 为了让小鸟在进入游戏时就能自动飞行,请务必勾选编辑器上方的 Autoplay on Load(加载时自动播放)按钮,并确认 Loop(循环)模式已开启。
图示显示了动画设置完成后的效果。
4.1.4 设置粒子特效(飞行残影)
为了让小鸟的飞行更具动感,我们计划为其添加“飞行残影”效果。残影的视觉原理其实是在小鸟移动的路径上,快速生成并淡出一系列小鸟的图像。相比于用复杂的代码去手动绘制每一帧,使用 Godot 的粒子系统(CPUParticles2D)会更加高效且易于调节。
请按照以下步骤配置粒子系统:
- 添加节点: 在
Bird节点下新建一个子节点,搜索并选择CPUParticles2D。 -
配置基础发射参数: 在右侧属性面板中进行如下设置,以模拟向后飘散的效果:
- Amount(数量): 设置为 8,这决定了同时存在的残影数量。
- Lifetime(生命周期): 设置为 0.2s,即每个残影只存在极短的时间。
- Texture(纹理): 拖入
bird1.png,让残影的形状与小鸟保持一致。 - Direction(方向): 设置为 (-1, 0),使粒子向左方喷射。
- Spread(分叉角度): 设置为 0,保证残影没有分散。
- Gravity(重力): 设置为 (0, 0),防止残影因重力下坠。
- Initial Velocity(初始速度): Min 和 Max 均设置为 150。
- Show behind parent(在父节点后显示): 开启(On),这样残影会被小鸟本体遮挡,视觉上更自然。
-
设置缩放曲线(逐渐变小):
- 找到 Scale Amount Curve 属性,点击 New Curve。
- 在弹出的曲线编辑器中,将起始点(左侧)保持在 1.0,将结束点(右侧)拉低至 0.5。这表示残影在消失前会缩小到原来的一半。
-
设置透明渐变(逐渐消失):
- 找到 Color Ramp 属性,点击 New Gradient。
- 在渐变色控制条中,点击左侧滑块确保颜色为纯白(Alpha 为 255)。
- 点击右侧滑块,将颜色的 Alpha(透明度) 通道拉至 0。这会让残影产生从清晰到完全透明的过渡效果。
-
初始状态控制: 默认将 Emitting(发射) 属性设置为关闭。我们希望只有当小鸟飞起来时,才通过代码开启这个特效。
上图显示了设置缩放曲线完成后效果。
上图显示了设置透明渐变完成后效果。
4.1.5 设置音效节点
声音是游戏反馈的重要组成部分。我们需要为小鸟配置两个独立的音频通道,以便分别处理飞行和得分的声音。
- 添加节点: 为根节点添加两个
AudioStreamPlayer2D子节点。 - 重命名: 分别命名为
FlySound(用于扑翼声)和ScoreSound(用于得分奖励音)。 - 说明: 你可以在属性面板的 Stream 选项中直接指定
.wav文件。在本例中,我们暂时保持为空,稍后会在脚本逻辑中动态调用它们,以实现更灵活的控制。
4.1.6 设置节点分组(Group)
在游戏逻辑中,当碰撞发生时,管道或地面需要知道“是谁撞了我”。为了避免繁琐的路径引用,我们使用 Godot 的分组机制为小鸟打上一个身份标签。
- 进入分组面板: 选中
Bird根节点,在编辑器右侧属性面板旁点击 Node(节点) 标签页,然后选择 Groups(分组)。 - 添加标签: 在输入框中输入
bird,点击 Add(添加)。

为什么要这样做? 当管道检测到碰撞时,它只需要检查对方是否在
bird分组中。如果是,就触发游戏结束。这种方式让代码变得非常通用,即使你以后把主角换成一只“小猫”,只要它在bird组里,所有的碰撞逻辑依然有效。
4.1.7 编写控制脚本
小鸟的所有节点现已配置就绪,现在我们需要通过 GDScript 脚本来赋予它“灵魂”。点击 Bird 根节点右上角的“附加脚本”按钮,创建并保存 bird.gd。
我们将代码分为两个部分来解析:变量声明与物理逻辑实现。
(1)变量与常量定义
首先,在脚本顶部定义游戏所需的物理参数和资源引用:
extends CharacterBody2D
# 物理常量
const JUMP_VELOCITY = -500.0 # 向上飞行的瞬时速度(y轴负方向为上)
const GRAVITY = 1500 # 模拟重力加速度
# 预加载音效资源,提高运行效率
const HIT = preload("res://assets/hit.wav")
const POINT = preload("res://assets/point.wav")
const WING = preload("res://assets/wing.wav")
var rot_degree = 0 # 用于记录当前的旋转角度
var is_dead = true # 死亡状态标识,默认死亡(等待游戏正式开始)
@export var max_speed := 700 # 最大下落速度限制,使用 @export 方便在编辑器中微调
@onready var animated_sprite_2d = $AnimatedSprite2D
@onready var cpu_particles_2d = $CPUParticles2D
@onready var fly_sound: AudioStreamPlayer2D = $FlySound
@onready var score_sound: AudioStreamPlayer2D = $ScoreSound
- 常量 (const):
JUMP_VELOCITY决定了小鸟跳得有多高,而GRAVITY决定了它掉落的速度。通过preload加载音效,可以确保声音在播放时不会产生延迟。 - 状态变量:
is_dead是一个关键开关。当它为true时,小鸟会处于“静止”状态,只有游戏正式开始或复活后,它才会响应物理逻辑。 - 节点引用 (@onready): 这些变量通过
$符号直接链接到我们之前创建的子节点,方便在后续代码中控制动画、粒子和声音。
(2)物理循环逻辑
接着,我们通过 _physics_process 函数来实现每一帧的运动计算。这个函数是处理物理同步的最佳场所。
func _ready():
# 暂时留空,后续用于初始化逻辑
pass
func _physics_process(delta):
# 只有当小鸟存活时,才处理物理逻辑
if not is_dead:
# 1. 应用重力:速度 = 加速度 * 时间
velocity.y += GRAVITY * delta
# 2. 处理玩家输入:检测到名为 "fly" 的动作按下
if Input.is_action_just_pressed("fly"):
velocity.y = JUMP_VELOCITY # 给小鸟一个向上的速度
fly_sound.stream = WING # 设置扑翼音效
fly_sound.play() # 播放声音
# 3. 计算旋转角度:根据当前纵向速度改变仰俯角
# 使用 clampf 将旋转限制在 -30° 到 30° 之间,避免“翻车”
rot_degree = clampf(-30 * velocity.y / JUMP_VELOCITY, -30, 30)
rotation_degrees = rot_degree
# 4. 速度限制:确保下落速度不会无限增加
velocity.y = clampf(velocity.y, -max_speed, max_speed)
# 5. 执行移动:根据 velocity 自动处理碰撞与滑动
move_and_slide()
代码解释:
- 重力模拟:
velocity.y += GRAVITY * delta是物理公式\(v = v_0 + at\)的代码实现。delta确保了无论玩家电脑帧率高低,小鸟下落的速度在现实时间中都是一致的。 - 输入响应: 这里使用了
Input.is_action_just_pressed。请确保你在“项目设置 -> 输入映射”中已经创建了一个名为 "fly" 的动作,并绑定了鼠标左键或空格键。 - 动态姿态: 我们根据小鸟的 Y 轴速度来计算旋转角度。当小鸟向上冲时,它会抬头;当它加速下落时,它会低头。
clampf函数确保了这种旋转是温和且受控的。 - 内置函数
move_and_slide(): 这是CharacterBody2D的核心方法。它会自动读取我们修改过的velocity(速度)变量,处理小鸟与地面或管道的摩擦、滑动和碰撞,无需我们手动计算坐标。
至此,我们完成了小鸟场景的搭建与核心逻辑控制。下一步将继续实现管道障碍场景,为游戏加入挑战机制。
4.2 构建障碍场景:管道与金币
在《Flappy Bird》中,管道障碍是核心的挑战来源。每一组障碍其实都是一个复合体,由一根“上管道”、一根“下管道”以及中心处的一枚“金币”组成。它们会源源不断地从右侧生成并向左移动。本节我们将通过场景嵌套的方式,先制作单根管道,再组合成完整的障碍组。
4.2.1 制作基础管道组件
为了提高开发效率,我们先制作一个基础管道场景,后续通过旋转和位移将其变成上下两根管道。
- 创建场景: 新建场景,根节点选择 Area2D(用于检测碰撞),重命名为
Pipe。 - 添加组件: 为其添加两个子节点:Sprite2D(视觉显示)和 CollisionShape2D(物理碰撞)。
- 调整精灵属性:
- 将
pipe.png拖入 Sprite2D 的 Texture 属性。 - 关键设置: 在属性面板中取消勾选 Centered。默认情况下图片的中心点在正中央,取消后轴心会位于左上角。
- 轴心偏移: 将 Offset.x 设置为 -39。这样做能让轴心(锚点)精准地定位在管道开口边缘的中心位置,极大方便了后续的对齐与旋转操作。
- 将
- 设置CollisionShape2D: 设置shape为RectangleShape2D,使用鼠标来操作矩形周边的小圆点(控制点),调整它的大小让它和上部管道的图形重合。完成后保存为
pipe.tscn。
完成设置后的场景如上图所示。
4.2.2 组装完整障碍场景
现在,我们将刚才制作的单根管道组合在一起。
- 场景构建: 新建一个场景,根节点选择 Node2D,命名为
Pipes。 - 嵌套子场景: 从文件系统中将
pipe.tscn拖入当前场景两次,分别重命名为PipeTop(上管道)和PipeBottom(下管道)。 - 布局与对齐:
- PipeTop(上管道): 将 Position.y 设置为 90。
- PipeBottom(下管道): 将 Position.y 设置为 -90,并将 Rotation 设置为 180 度进行翻转。
- 这样处理后,上下两根管道之间正好留出了一个 180 像素 的空隙,供小鸟通过。将此场景保存为
pipes.tscn。
4.2.3 添加金币与得分机制
金币不仅是分数的来源,其动感效果也能提升游戏品质。我们需要实现“常态旋转”和“获得后消失”两种动画。
- 构建金币节点: 在Pipes根节点根节点下添加 Area2D 子节点,命名为 Coin
- 给Coin添加两个子节点:
- AnimatedSprite2D:用于显示金币旋转动画
- CollisionShape2D:用于与小鸟检测是否通过得分
- 设置金币旋转动画:
- 点击 Sprite Frames → 新建 SpriteFrames 资源
- 将 coin-frame-1.png 到 coin-frame-11.png 拖入动画帧
- 打开 Autoplay 和 Looping,让金币在一开始就自动播放并循环播放
- 由于素材较大,将scale设置为0.3
- 设置 FPS = 10
- 设置CollitionShape2D:使用 CircleShape2D,半径与缩放后的金币大小匹配即可。
完成设置后如上图所示
我们还需要添加金币收集后消失的动画,步骤如下:
- 给Coin添加子节点 AnimationPlayer
- 底部面板中会出现动画编辑区,点击Animation,增加一个名为coin的动画,动画时间设置为0.5秒。
- 点击Add Track添加动画轨道,选择Property Track,此时弹出对话框,选择AnimatedSprited2D,点击OK,再搜索scale属性,找到scale属性后选择Open。这样会创建一个动画轨道,它会控制金币的缩放比例。
- 后续的操作和第三章介绍的操作一样。在开始和结束时间建立两个关键帧。开始时间scale值为0.3,结束时间scale值为0.8。
- 类似的操作再增加一个Property Track,找到AnimatedSprited2D的modulate属性,开始时间的关键帧的颜色设置为白色,结束时间的关键帧的颜色设置为透明。
点击播放动画,你会看到硬币放大后淡出的效果。将时间轴还原到开始状态保存场景。
所有的设置完成后如上图所示。
4.2.4 屏幕外自动销毁
由于管道会源源不断地生成,如果不及时清理离开屏幕的管道,游戏运行一段时间后就会因为对象过多而变得卡顿。我们需要知道什么时候,Pipes场景离开了屏幕。一种方法是在代码中通过Pipes的position坐标来实现这个逻辑,另一种方法是使用一种特别的检测节点。操作步骤如下:
- 添加检测节点:给Pipes根节点增加一个子节点,类型为VisibleOnScreenNotifier2D。
- 节点作用:此节点有两个内置信号:
- screen_entered:进入屏幕时触发
- screen_exited:离开屏幕时触发(我们将用这个信号)
- 它能自动检测自身是否处于屏幕可见区域,非常适合判断“对象是否应被销毁”。你也可以在该节点上点右键,Open Document来阅读相关的帮助文档。
帮助文档如上图所示。
障碍场景的“骨架”和“特效”都已经搭建完毕,我们下面需要给它增加代码逻辑。
4.2.6 编写障碍逻辑代码(pipes.gd)
在完成了 Pipes 场景的搭建后,我们需要通过脚本赋予它“运动”的能力,并处理与小鸟的交互逻辑。点击 Pipes 根节点,创建并保存 pipes.gd 脚本。
(1)初始化变量与引用
首先定义管道的移动速度以及必要的子节点引用。
extends Node2D
const SPEED = -150 # 负值表示向左移动
var passed = false # 标记小鸟是否已经安全通过此管道,防止重复计分
@onready var coin: Area2D = $Coin
@onready var pipe_bottom: Area2D = $PipeBottom
@onready var pipe_top: Area2D = $PipeTop
@onready var animation_player: AnimationPlayer = $Coin/AnimationPlayer
@onready var visible_on_screen_notifier_2d: VisibleOnScreenNotifier2D = $VisibleOnScreenNotifier2D
(2)信号连接:建立交互网络
在 _ready 函数中,我们将各个触发器的信号连接到对应的处理函数上。这是实现“松耦合”架构的关键:
func _ready():
# 当小鸟进入上/下管道的区域时触发
pipe_bottom.body_entered.connect(on_pipe_body_entered)
pipe_top.body_entered.connect(on_pipe_body_entered)
# 当小鸟进入中间金币区域时触发
coin.body_entered.connect(on_coin_body_entered)
# 当整组管道完全离开屏幕时触发
visible_on_screen_notifier_2d.screen_exited.connect(on_exited)
(3)核心逻辑函数实现 接下来,我们通过三个函数来处理不同的碰撞和退出事件:
func on_pipe_body_entered(body):
# 检查碰撞体是否属于 "bird" 分组,且小鸟当前还活着
if body.is_in_group("bird") and not body.is_dead:
# TODO: 触发游戏结束逻辑,此处先用 pass 占位
pass
func on_exited():
queue_free() # 将管道从场景树移除并释放内存
func on_coin_body_entered(body):
# 如果是小鸟通过,且之前还没计过分
if body.is_in_group("bird") and not passed:
passed = true # 立即设为已通过
animation_player.play("coin") # 播放我们之前制作的“金币放大淡出”动画
# 关键点:等待动画播完再处理后续逻辑
await animation_player.animation_finished
coin.queue_free() # 动画播完后,安全删除金币节点
await是什么?await就像是给脚本按下了“暂停键”。它会告诉程序:“在这儿等着,直到动画播放结束的信号(animation_finished)传过来,你再接着跑后面的代码。”这避免了金币还没闪烁完就消失的尴尬。 在GDScript中,await 关键字用于等待。它常与信号(signal)一起使用,例如:当我们希望等待一个计时器到时间,或一个动画播放结束,可以写成await timer.timeout或await animation_player.animation_finished。这意味着:当前函数会暂停在这一行,直到信号被触发才继续往下执行。这种机制非常适合实现“延迟后执行某件事”的逻辑。在实际复杂项目中需注意原本的节点是否存在,如果在动画播放中节点被意外删除了,这个 await 可能会抛出异常。
(4)管道移动
最后,在 _process 每一帧的循环中更新管道的水平位置:
至此,我们完成了障碍场景的构建。这个模块不仅处理了视觉元素(上下管道、金币动画),也承担了核心逻辑(碰撞、得分、销毁)功能,是整个游戏玩法的关键环节。下一步继续实现 UI 场景,为玩家提供反馈界面与开始按钮。
4.3 HUD用户界面实现
在任何游戏中,用户界面(UI,User Interface)都扮演着向玩家传递信息与提供交互的关键角色。虽然《Flappy Bird》的 UI 风格极其简约,但麻雀虽小五脏俱全,它必须承担起显示实时得分、提示游戏状态(如欢迎语或游戏结束)以及提供“开始按钮”的任务。
在 Godot 中,所有的 UI 元素都属于 Control(控件) 类型节点。为了确保这些界面元素始终固定在屏幕上,不随摄像机的移动而位移,我们通常会将它们放置在独立的 CanvasLayer 渲染层中。
4.3.1 创建HUD场景
我们将利用 Godot 强大的“容器(Container)”系统来自动处理排版,这样无论玩家的屏幕分辨率如何变化,UI 都能保持美观。
- 创建根节点: 新建一个场景,根节点选择 CanvasLayer节点,并命名为
HUD。这个节点就像是一块透明的玻璃,盖在游戏世界(2D 空间)上方,专门负责渲染 UI。 - 设置安全边距: 在
HUD下添加 MarginContainer节点。它像一个相框,能防止文字紧贴屏幕边缘。- 选择MarginContainer节点,在设置中找到 Anchors Preset(锚点预设) ,选择 Full Rect(全矩形),让它铺满整个屏幕。
- 在属性面板的 Theme Overrides > Constants 中,将
margin_top/bottom/left/right全部设置为 20,预留出一定的间距。
- 自动垂直布局: 在 MarginContainer 下添加 VBoxContainer节点。这个容器非常聪明,它会自动将其下的所有子元素按照从上到下的顺序整齐排列,适合纵向堆叠 UI 元素。
- 最后将场景保存为
hud.tscn文件。
4.3.2 添加UI控件元素
现在,我们将“填入”具体的控件。请在 VBoxContainer 下依次添加以下节点,并按照说明进行配置:
| 控件类型 | 重命名 | 用途说明 |
|---|---|---|
Label |
ScoreLabel |
显示当前得分 |
Control |
无 | 空白填充,调整布局间距 |
Label |
Message |
显示欢迎提示或“Game Over” |
Button |
无 | 提供“开始游戏”操作 |
Control |
无 | 底部填充 |
具体设置如下:
- ScoreLabel 分数显示设置
- Text 设置为 "Score: 0",后续会通过代码来控制这里的属性,预先写入的内容是为了调试字体风格。
- Horizontal Alignment = Center
- Label Settings 是用于设置字体风格,点击New Label Setting新建样式资源,设置字体为LuckiestGuy-Regular.ttf,字号32,字色为白色,并添加阴影与描边,将这个样式保存为 white_32.tres 以方便后续复用。你会在文件系统中看到这个资源文件。如果这步操作有些困难,你也可以直接使用assets中已经制作好的资源文件。
- Message 提示信息设置
- 初始 Text 设置为 "Welcome"
- Label Settings处选择 quick load,选择使用 white_32.tres 样式资源
- 设置 Horizontal Alignment = Center
- Button 开始按钮设置
- Text 设置为 "Start Game"
- Container Sizing > Horizontal 设置为 Shrink Center,让按钮宽度自适应
- Theme Overrides设置其字体和大小,将字体文件拖入font,再将font size设置为20。
- Control 节点设置
- 将两个 Control 节点的Container Sizing -> Vertical 设置为 Fill
- 确保 Expand 勾选,表示自动占满可用空间
- 作用:使按钮区域居中分布,整体布局美观
得分控件的设置界面如上图所示
完成设置后的UI界面如上图所示
最后,选中 HUD 根节点,点击附加脚本图标并保存为 hud.gd。目前我们保持脚本为空,在后续实现整体游戏逻辑时,我们将在这里编写刷新分数和隐藏/显示按钮的代码。
4.4 背景与视差滚动实现
在《Flappy Bird》这类横向卷轴游戏中,背景的滚动是营造飞行错觉的关键。我们并不需要让小鸟在无穷无尽的世界中真实地“向右飞”,而是反其道而行之:让小鸟在 X 轴上保持相对静止,通过让背景和障碍物不断“向左移动”,利用视觉参考系的切换来创造出向前飞行的感觉。
为了提升画面的精致感,我们将引入视差滚动(Parallax Scrolling)技术。通过让远景(天空)移动得慢、近景(地面)移动得快,模拟出现实世界中“远小近大、远慢近快”的深度感。
4.4.1 什么是视差滚动
视差滚动是一种经典的 2D 视觉欺骗技术。通过将背景拆分为多个独立的图层,并为每个图层设置不同的移动速度,从而在平面的屏幕上制造出三维的景深效果。
Godot 提供了专门的节点组合来轻松实现这一效果:
- ParallaxBackground:这是滚动的“总控制器”,负责管理所有的图层并控制它们的整体偏移。
- ParallaxLayer:这是“图层容器”,每一层都可以独立设置滚动系数(Motion Scale)。
- Sprite2D:这是实际的图像承载者,用于显示背景图或地面贴图。
4.4.2 使用“场景继承”来组织背景结构
本游戏中的背景系统分为两个背景层来实现视差效果。 - 天空背景:贴图 bg.png,位于最底层,由于距离极远,它应该移动得非常缓慢。 - 地面图层:贴图 ground.png,位于中层,它需要遮挡住管道的下端,且移动速度必须与管道完全同步,其速度要比天空快。
按照常规做法,我们可以在一个控制器下放两个图层。但考虑到渲染层级(Z-Index),天空必须在所有物体之后,而地面必须显示在管道之上,因此我们将它们拆分为两个独立的 ParallaxBackground 场景。
我们可以直接制作天空和地面这两个场景,不过为了让大家熟悉场景继承的概念,我们先做一个“背景模板”,再由此派生出天空和地面。
4.4.3 创建基础背景场景
首先,我们建立一个通用的父类场景,用于统一管理滚动逻辑:
- 新建场景,根节点为 ParallaxBackground,重命名为 BG。
- 添加子节点 ParallaxLayer,并在其下添加 Sprite2D子节点。
- 给 BG 场景添加脚本 bg.gd,用于设置滚动逻辑,代码中只需要定义滚动速度和定义_process函数即可。
const SPEED = -150 # 基础移动速度
func _process(delta):
# scroll_offset 是内置属性,用于控制背景的位移
scroll_offset.x += SPEED * delta
scroll_offset.x,我们能让背景以每秒 150 像素的速度持续向左循环滚动。

这样我们的基础场景就完成了,保存为bg.tscn文件。
4.4.4 通过继承创建天空与地面
有了“模板”后,我们可以通过继承快速生成具体的场景。先来创建天空场景。
- 点击菜单 Scene > New Inherited Scene,开始新建继承场景。
- 在对话框中选择要继承的目标场景为bg.tscn,这样会创建一个新的子场景。你会看到新的场景节点是黄颜色的。
- 将根节点改名为Sky,保存这个场景,命名为sky.tscn。
- 在Sky场景中选择Sprite2D节点,将bg.png文件拖入Texture属性中,再把Offset属性中Centered的勾选取消掉,这样图片的左上角就会位于屏幕的原点位置。
- 设置ParallaxLayer节点的属性:
- Motion > Scale 设置为 0.2,这意味着天空的实际滚动速度只有地面的 20%,视觉上会显得非常深远。
- Motion > Mirroring 设置为 (864, 0),因为天空图片的尺寸是864*768,这样的设置是指当运动了864个像素后,再渲染一张新的图片。这样无缝循环可以实现无限背景的效果。
天空场景设置完成后如上图所示。
用同样的操作步骤,我们再继承创建地面子场景。
- 再次继承 bg.tscn,命名为 ground.tscn
- 将 Sprite2D 的贴图设置为 ground.png
- 取消 Centered,并将 Offset.Y = 700,让地面贴图显示在屏幕底部附近
- 设置 ParallaxLayer:
- Motion > Scale 设为 1.0,表示地面速度与障碍一致
- Motion > Mirroring 设置为 (900, 0),与地面图宽度一致
地面场景设置完成后如上图所示。
好了,通过这种基于继承的设计,我们不仅实现了极具层次感的视觉效果,还通过 Mirroring 机制解决了背景断裂的问题。现在天空和地面已经准备就绪。
5 全局变量和信号管理
在一个完整的游戏项目中,各个模块往往不是孤立存在的。小鸟撞击了管道,背景需要停止滚动;玩家点击了 UI 按钮,小鸟需要被激活。为了实现模块间的通信与协调,我们需要一个“总指挥”来统一管理这些状态和事件。在 Godot 中,我们可以使用Autoload(自动加载)脚本和信号机制来完成这项工作。
5.1 单例和全局管理
在目前的游戏中,三个核心模块存在着紧密的协作需求。我们可以通过下表理清它们之间的“对话”逻辑:
| 需求 | 谁触发 | 谁响应 |
|---|---|---|
| 游戏开始 | HUD 按钮点击 |
Bird 激活、Pipes 开始生成 |
| 得分事件 | Pipes 中金币被吃掉 |
HUD 更新分数、播放音效 |
| 游戏结束 | Bird 撞到管道或地面 |
所有模块停止动作、显示结算界面 |
我们需要一个能在游戏开始时自动加载的节点,来管理游戏的全局变量和信号。
在 Godot 引擎中,Autoload节点是一种特殊的节点,它是一种在游戏启动时自动加载,并在整个生命周期内始终存在,而且能被任何脚本随时访问的“超级节点”。这使得它们非常适合用于管理全局数据、游戏状态、资源管理和事件处理等。这种编程方式也称为“单例模式”,类似于整个游戏里只会有一个“导演”,他管理全局数据、控制游戏流程。
5.2 创建全局管理器
我们将创建一个名为 GameManager 的“导演”,负责记录分数并分发核心信号。
第一步:创建脚本文件
在文件系统中新建一个名为game_manager的文件夹,然后在这个文件夹中新建一个名为game_manager.gd的脚本文件。代码内容如下:
extends Node
# 定义三种全局信号
signal GameStart # 游戏开始
signal GameOver # 游戏结束
signal UpdateScore # 分数变动
var score = 0 # 全局分数变量
func _ready():
# 监听信号连接到响应函数,用于处理数据层面的更新
UpdateScore.connect(add_score)
GameOver.connect(on_game_over)
func add_score():
# 需要更新分数时,将分数加1
score += 1
func on_game_over():
# 游戏结束时重置分数
score = 0
第二步:设置自动加载(Autoload)
让脚本生效的关键步骤是将其注册到引擎的单例列表中:
- 打开菜单:Project > Project Settings > Global
- 在Global下找到 Autoload(自动加载) 选项卡
- 点击文件夹图标,选择 game_manager/game_manager.gd,它的name会自动设置为GameManager
- 点击Add 完成设置。
现在,你可以在项目的任何脚本中直接输入 GameManager 来调用其中的信号和变量,就像调用内置函数一样方便。
完成Autoload设置后如上图所示。
5.3 使用全局管理器
有了总指挥,现在我们需要回到各个场景中,让它们接入这套信号网络。
修改Bird场景
回到Bird场景,在bird.gd代码中增加如下内容。
func _ready():
# 连接三种全局信号
GameManager.GameOver.connect(on_game_over)
GameManager.UpdateScore.connect(on_get_score)
GameManager.GameStart.connect(on_game_start)
func on_game_start():
is_dead = false # 恢复行动能力
cpu_particles_2d.emitting = true # 开启飞行残影
func on_get_score():
score_sound.play() # 仅负责播放声音,分数计算交给 GM
func on_game_over():
fly_sound.stream = HIT # 切换为撞击声
fly_sound.play()
cpu_particles_2d.emitting = false
is_dead = true # 停止响应输入
代码解释:
- on_game_start函数负责在游戏开始时,修改小鸟状态,这样粒子系统会打开。
- on_get_score函数负责在得分时,播放得分音效。
- on_game_over函数负责在游戏结束时,播放死亡音效,关闭粒子系统,并且修改小鸟的is_dead状态。
修改Pipes场景
回到Pipes场景代码,管道通过发射(emit)信号来通知全局发生了什么,它不需要知道谁在听,只管汇报。修改pipes.gd文件如下。
func on_pipe_body_entered(body):
if body.is_in_group("bird") and not body.is_dead:
GameManager.GameOver.emit() # 撞到了!通报游戏结束
func on_coin_body_entered(body):
if body.is_in_group("bird") and not passed:
passed = true
GameManager.UpdateScore.emit() # 穿过了!通报得分
animation_player.play("coin")
await animation_player.animation_finished
coin.queue_free()
代码解释:
- on_pipe_body_entered函数中,当管道障碍检测到碰撞到小鸟时,触发GameOver信号。
- on_coin_body_entered函数中,当金币检测到碰撞到小鸟时,触发UpdateScore信号。
- 这样内置信号通过响应函数触发了自定义信号的触发,让其它场景可以响应这些信号。这是一种信号传递的技巧。
修改HUD场景
UI 模块是信息的展示窗,它通过监听信号来切换界面状态。修改hud.gd文件如下。
# 定义节点引用变量
@onready var score_label = $MarginContainer/VBoxContainer/ScoreLabel
@onready var message = $MarginContainer/VBoxContainer/Message
@onready var button: Button = $MarginContainer/VBoxContainer/Button
func _ready():
# 初始化显示
update_score()
# 信号连接
GameManager.GameOver.connect(on_game_over)
GameManager.UpdateScore.connect(update_score)
button.pressed.connect(on_pressed)
func update_score():
# 从全局变量中读取最新的分数,并修改文本显示
score_label.text = "Score: %s" %str(GameManager.score)
func on_game_over():
update_score()
message.text = "GAME OVER"
message.show()
button.show() # 显示开始按钮,允许玩家重开
func on_pressed():
# 点击按钮,通知游戏开始
GameManager.GameStart.emit()
# 隐藏Message和Button两个元素
message.hide()
button.hide()
这样我们就完成了所有信号的传递,再归纳一下信号的使用方法。信号分为内置信号和自定义信号。内置信号只需要管发射和连接即可,自定义信号则需要三个步骤。
- 定义信号:在
GameManager中使用signal关键字宣告事件。 - 连接信号:在
Bird或HUD的_ready中使用connect连接关注事件,并绑定响应函数。 - 发射信号:在
Pipes或Button交互时使用emit宣告事件发生。
这种模式让场景之间实现了“相忘于江湖,却感应于信号”的高级协作。现在,所有的零件都已经通过无线电联系在一起了,我们只需要最后一步:在主场景中把它们实例化出来!
6. 主场景整合与游戏流程控制
经过前面的构建,我们已经完成了所有核心模块:主角小鸟、障碍管道、用户界面、背景系统和全局管理器。现在,我们将把这些零散的场景组装成一个有机整体,并编写主逻辑来掌控游戏的生死循环。
6.1 搭建舞台:创建主场景结构
- 新建场景,根节点选择 Node2D,将其重命名为 Main
- 实例化子场景,点击节点面板上方的“实例化子场景”按钮(链条图标),依次将以下场景添加为
Main的子节点:- sky.tscn(背景)
- ground.tscn(地面)
- bird.tscn(主角)
- hud.tscn(用户界面)
- 调整空间层级与布局
| 节点名称 | 调整建议 |
|---|---|
Bird |
设置位置为 (160, 300),居中偏左,给玩家留出足够的反应空间。 |
Ground |
在检查器中找到 Canvas Layer → Layer,将其设置为1。这能确保地面始终覆盖在管道的前方 |
HUD |
保持默认即可。作为 CanvasLayer,它会自动漂浮在所有游戏元素的最高层。 |
6.2 添加管道障碍系统
为了让管道源源不断地生成,我们需要在 Main 场景中添加一些功能节点:
-
添加管道障碍容器:给Main场景新增一个Node2D子节点,重命名为PipesGroup。这只是一个空节点,专门用来存放运行时通过代码动态生成的管道实例,方便我们后期统一清空或管理。
-
添加管道障碍出生点:在Main场景中再增加一个Marker2D类型的节点,这个节点用于Pipes场景的出生位置,在属性面板中修改position为(600,350)。这样Pipes会在屏幕右侧中间的位置出生。
- 添加障碍生成定时器:添加子节点 Timer,用于每隔一段时间生成一个管道障碍,设置属性如下:
- Wait Time = 2,每隔2秒就会触发Timer的timeout信号,也就是每2秒生成一个障碍
- Autostart = false,不自动启动,我们希望游戏点击“开始”后,计时器才开始工作。
- One Shot = false,一次性设置为否,表示要重复触发
6.3 添加边界检测节点
如果小鸟飞得太高掉出了屏幕顶部,或者摔得太狠掉出了屏幕底部,游戏也应该结束。我们需要在顶部和底部添加不可见的碰撞区域:
- 在Main场景中添加两个 Area2D 节点,分别命名为 Ceil (天花板)和 Floor(地板)。
- 给它们各自添加 CollisionShape2D 子节点,形状使用 RectangleShape2D
- 将它们放置在屏幕上下边缘,宽度覆盖整个窗口,用于检测碰撞
- 当小鸟接触这些区域时,也会触发游戏失败逻辑。
6.4 添加背景音乐节点
我们还需要添加一个音乐节点,用于播放背景音乐。
- 在Main场景中增加 AudioStreamPlayer 节点
- 将 BGMUSIC.mp3 拖入 Stream 属性
- 勾选 Autoplay,并可设置 Volume dB = -10 以调整音量
6.5 编写主场景逻辑
所有的节点都已经准备好,将场景保存为main.tscn文件。接下来就是代码的编写了。给Main根节点附加代码,保存为main.gd文件。
const GAP = 180 # 表示随机波动范围
# 预加载管道蓝图,用于实例化Pipes场景。
var pipes_scene: PackedScene = preload("res://pipes/pipes.tscn")
# 定义节点引用变量
@onready var timer: Timer = $Timer
@onready var spawn_point: Marker2D = $SpawnPoint
@onready var bird: CharacterBody2D = $Bird
@onready var ceil: Area2D = $Ceil
@onready var floor: Area2D = $Floor
@onready var pipes_group: Node2D = $PipesGroup
func _ready():
# # 建立全局信号连接
GameManager.GameOver.connect(on_game_over)
GameManager.GameStart.connect(on_game_start)
# 监听边界碰撞与定时器触发
ceil.body_entered.connect(on_border_body_enter)
floor.body_entered.connect(on_border_body_enter)
timer.timeout.connect(on_timer_timeout)
func on_game_start():
new_pipes() # 立即生成第一组管道
timer.start() # 启动定时器
func on_game_over():
timer.stop() # 停止生成新管道
reset_game() # 重置游戏
func on_timer_timeout():
new_pipes() # 闹钟响了,生成新管道
func on_border_body_enter(body):
# 检测飞出边界的是否是小鸟,触发游戏结束信号。
if body.is_in_group("bird") and not body.is_dead:
GameManager.GameOver.emit()
# 核心功能:实例化管道
func new_pipes():
# 1. 根据蓝图克隆一个管道实例
var pipes = pipes_scene.instantiate()
# 2. 计算随机高度:在出生点一定范围内随机波动
var pos_y = randf_range(spawn_point.position.y - GAP,
spawn_point.position.y + GAP)
# 3. 设定坐标
pipes.global_position.x = spawn_point.position.x
pipes.global_position.y = pos_y
# 4. 将其放入收纳盒,管道开始在自身代码驱动下向左移动
pipes_group.add_child(pipes)
# 清理现场重开游戏
func reset_game():
bird.global_position = Vector2(160,300) # 归位小鸟
# 清空所有还在路上的管道
var pipes = pipes_group.get_children()
for pipe in pipes:
pipe.queue_free()
6.6 运行与调试
现在,点击编辑器右上角的“运行当前场景”按钮(或按 F6)。我们可以开始游戏了。游戏场景如下。

通过以上结构,我们实现了完整的游戏流程:
- 开始阶段,UI显示欢迎界面,玩家点击“Start Game”按钮,发出 GameStart 信号。小鸟激活,定时器启动,障碍开始生成和移动。
- 游戏阶段,小鸟在重力影响下坠落,玩家操作小鸟跃起。每当成功穿越管道,得分发出 UpdateScore 信号。
- 结束阶段,一旦小鸟碰撞障碍或地板,小鸟变回死亡状态,发出 GameOver 信号。管道停止生成,UI 弹出 GAME OVER。点击按钮,现场清空,一切周而复始。
7. 本章使用节点回顾
在开发《Flappy Bird》的过程中,我们深度接触了 Godot 引擎中许多核心节点。理解这些节点的功能与应用场景,不仅能帮你更高效地复刻经典,还能为你今后构思原创项目打下扎实的基础。本节将对本章涉及的节点进行分类回顾,助你建立结构化的知识体系。
7.1 物理与碰撞节点
Godot 的物理节点是游戏交互的核心。它们被统一归类为 PhysicsBody(物理主体),但各自的性格截然不同。
- CharacterBody2D,它是一个完全受代码控制的物体,可以进行移动,旋转,碰撞检测等逻辑,而且它会和其它的物体进行物理反馈。提供
velocity属性和move_and_slide()方法。在本项目中,我们选择 CharacterBody2D 是为了精确控制飞行逻辑与状态,便于实现复杂输入响应。 - StaticBody2D,顾名思义,它是静止不动的,通常用于地面或墙壁。它像一块坚硬的基石,为其他运动物体提供碰撞反馈。
- RigidBody2D,它像现实中的足球,完全交由物理引擎接管。设置重力和初速度后,它会自动模拟翻滚、弹跳。
| 节点类型 | 物理行为 | 控制方式 | 应用示例 |
|---|---|---|---|
| StaticBody2D | 静止 | 无需控制 | 地面、墙体 |
| RigidBody2D | 受力运动 | 由物理引擎控制 | 掉落物、足球 |
| CharacterBody2D | 可移动 + 碰撞 | 由代码控制 | 玩家角色、小鸟 |
还有一些节点不参与到物理模拟,但会和碰撞检测有关:
- Area2D:用于检测节点是否进入、离开、重叠等区域事件,不产生物理碰撞作用。它更像是一个“感应门”,当物体进入时发出信号。适合触发事件(如得分、游戏结束)。
- CollisionShape2D:用于定义物体的碰撞区域,它是上述所有节点的“感知外壳”,没有它,物理节点就变成了不可见的幽灵。
7.2 动画节点
前面章节中我们提到了不同的动画类型,在我们的游戏项目中就使用了逐帧动画与关键帧动画,这两种动画对应了Godot中两种动画节点:
- AnimatedSprite2D节点:用于播放逐帧动画,擅长处理由多张图片组成的序列。通过配置
SpriteFrames资源。可设置autoplay和loop实现自动循环播放。 - AnimationPlayer节点:用于关键帧动画,可对节点的属性(如缩放、位置、透明度)进行关键帧控制。适合制作 UI 动效、金币收集淡出效果、角色特效等。
7.3 粒子节点
粒子系统是提升游戏“打击感”和视觉表现力的利器。它可以模拟一系列复杂的自然现象和动态效果,如火焰、烟雾、水流、雪、爆炸、火花等。这些效果通常由大量的小元素组成,每个粒子都遵循一定的物理规则或随机行为模式,从而共同形成一个逼真的整体视觉效果。Godot中粒子节点可以通过设置贴图、速度、方向、生命周期、重力等参数,创建出多样的视觉效果。Godot中有两种粒子系统节点:
-
CPUParticles2D 是快速实现简单粒子效果的开发者的理想选择。这种类型的粒子系统由 CPU 管理,适合粒子数量较少、逻辑简单的效果(如灰尘、简单的烟雾或本项目中的残影)。它最大的优势是跨平台兼容性极佳。
-
GPUParticles2D 是由 GPU 加速渲染,适合处理数以万计的粒子(如暴雨、大规模爆炸)。虽然效果华丽,但在移动端或网页端的兼容性需要开发者额外关注。
7.4 用户界面与布局节点
在 UI 开发中,最忌讳的是使用“硬编码坐标”来固定位置,因为这会导致游戏在不同分辨率的屏幕上出现排版错乱。Godot 强大的 Control 控件系统 为这一难题提供了完美的解决方案。
理解画布层
首先需要理解 CanvasLayer(画布层) 的核心作用。Godot 的渲染系统默认所有 2D 节点都在同一画布层(Canvas)中进行绘制,渲染顺序取决于节点在场景树中的顺序。
通常游戏中会将 UI 元素放置CanvasLayer中,它会在一个独立的 2D 渲染层上,从而不受视差、摄像机或主场景滚动影响。如此一来,需要在前景活动的分数、提示、按钮等UI元素,就会始终“安静的漂浮”在玩家视线的最前端。此外,还可以利用 Layer 属性以精确控制不同 UI 模块(如 HUD、弹出对话框、暂停菜单)之间的覆盖层级。
常见的用户界面节点
在具体实现上,所有的 UI 元素都派生自 Control 这一基类。Control 节点本身虽不显示内容,但它是所有 UI 交互逻辑的基石,支持尺寸定义、对齐规则以及信号传递。
为了实现整齐的布局,我们通常会配合使用各种容器节点:VBoxContainer 能够像磁铁一样将子控件沿垂直方向整齐排列,是制作设置列表或计分牌的利器。同理,HBoxContainer 负责水平排列,GridContainer 则用于更复杂的网格布局);MarginContainer 则负责处理“留白”,通过设置统一的边距,防止 UI 元素尴尬地贴在屏幕边缘。
而在容器内部,我们最常用的具体控件是 Label(标签) 和 Button(按钮)。Label 不仅能显示静态文本,还能通过 LabelSettings 灵活定制字体、颜色及描边,实时展示动态分数;Button 则承担了主要的交互任务,通过监听其 pressed 信号,我们可以轻松触发“开始游戏”或“重新启动”等逻辑。
布局属性
为了让 UI 真正具备“自适应”能力,理解布局的三大属性至关重要:
- Anchors Preset(锚点预设):锚点决定控件相对于父控件的位置和大小行为。常用设置包括:Top Left(固定在左上角)、Center(居中)、Full Rect(充满父容器)。
- Container Sizing(尺寸设置):控件在容器中的大小行为由 Horizontal 和 Vertical 两个方向控制;Fill是填满剩余空间;Shrink Center是按内容尺寸居中对齐
- Expand(扩展空间):如果勾选了 Expand 并设置 Fill,则控件将参与分配剩余空间。适用于按钮、提示文本等需要自动调节大小的控件。
7.5 背景滚动节点(视差效果)
为了在 2D 平面上营造出广阔的空间深度感,Godot 提供 ParallaxBackground 系统。这种“视差滚动”技术的原理是模拟现实视觉中“近快远慢”的自然现象。
- ParallaxBackground:背景总控制器,负责管理整体的偏移逻辑。可通过 scroll_offset 实现自动滚动。在其内部,我们会添加多个滚动图层。
- ParallaxLayer:单独的滚动图层,可设置 motion_scale(速度比例)与 mirroring(贴图重复)。例如,在本项目中,我们将天空层设置为
0.2,这意味着当摄像机移动时,它只以 20% 的速度缓慢漂移;而地面层设置为1.0,与玩家运动完全同步。配合mirroring(镜像)属性设置贴图宽度,背景便能实现无缝循环,创造出永无止境的飞行错觉。
7.6 其它辅助节点
除了上述核心系统,还有一些不可或缺的辅助节点在后台默默工作:
- Timer:定时器,它是游戏节拍的控制者。通过设置
wait_time并监听timeout信号,我们可以周期性地触发逻辑,例如每隔两秒在屏幕右侧随机产生一组管道。 - Marker2D:标记点,这是一个在编辑器中可见、但在运行时完全透明的坐标参考。它通常被用作物体的“出生点”,让我们在设计关卡时能够直观地放置位置,而不需要在代码中手写复杂的坐标数值。
- Node2D:所有 2D 节点的通用基类,承载了位置、旋转和缩放信息。可用于逻辑组织和空间定位。
- Node:所有节点的基础类型,不具备空间属性,常用于逻辑组织、信号传递、脚本挂载等。
本章小结
本章通过复刻经典的《Flappy Bird》游戏,带领读者完整体验了一个 2D 游戏从无到有的开发过程。这不仅是对前几章知识的巩固,也是一次系统性的实战演练。通过这个案例,我们学会了如何将概念转化为具体实现,并掌握了 Godot 引擎中多个关键技能。
在项目实施中,我们完成了以下内容:
- 项目搭建:创建游戏项目,导入资源,配置窗口与基本参数。
- 模块拆分:将游戏分解为主角、障碍、背景、UI 和主场景五大部分,明确各自功能与协作方式。
- 主角实现:使用
CharacterBody2D节点实现小鸟的物理运动、动画播放和粒子特效。 - 障碍构建:通过
Area2D和信号机制,实现管道与金币的碰撞检测与事件触发。 - 用户界面:搭建独立 UI 图层,展示得分、提示信息和操作按钮,并支持信号响应。
- 视差背景:使用
ParallaxBackground构建天空与地面分层滚动,增强飞行动感。 - 信号与全局控制:借助
GameManager单例实现模块间的解耦通信,统一管理游戏状态。 - 主场景整合:将所有模块集成于主场景中,驱动游戏逻辑,实现完整的“开始 → 得分 → 结束”循环。
通过本章学习,你不仅熟悉了 2D 游戏开发的基本流程,也初步建立了“模块化设计 + 脚本控制 + 信号通信”的开发思维模式。这将为你后续制作更复杂的游戏奠定扎实的基础。