跳转至

第五章:入门项目实战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 编辑器,按照以下步骤创建一个新的项目:

  1. 点击主界面的New Project按键新建项目。
  2. 项目名称输入 Flappy_Bird
  3. 选择一个你熟悉的目录作为项目保存路径。
  4. 渲染器选择默认的 Forward+(适合大多数 2D/3D 项目)。
  5. 点击“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)”动作。按照以下步骤配置:

  1. 打开设置:点击菜单栏的 Project(项目)-> Project Settings(项目设置)
  2. 切换选项卡:在弹出的窗口顶部,点击 Input Map(输入映射) 标签页。
  3. 创建动作:在 Add New Action(添加新动作) 的输入框中输入 fly,然后点击 Add(添加)
  4. 绑定按键
  5. 点击 fly 动作右侧的 “+” 号。
  6. 在弹出的监听窗口中,按下键盘上的 Space(空格键),点击 OK。
  7. 再次点击 “+” 号,切换到 Mouse Buttons(鼠标按钮),选择 Left Button(左键),点击 OK。
  8. 完成:现在,你的 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 引擎中为它们设计对应的场景结构。

alt text

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.pngbird2.pngbird3.png 这三张素材依次拖入右侧的帧列表中。
  • 开启循环播放: 为了让小鸟在进入游戏时就能自动飞行,请务必勾选编辑器上方的 Autoplay on Load(加载时自动播放)按钮,并确认 Loop(循环)模式已开启。

alt text 图示显示了动画设置完成后的效果。

4.1.4 设置粒子特效(飞行残影)

为了让小鸟的飞行更具动感,我们计划为其添加“飞行残影”效果。残影的视觉原理其实是在小鸟移动的路径上,快速生成并淡出一系列小鸟的图像。相比于用复杂的代码去手动绘制每一帧,使用 Godot 的粒子系统(CPUParticles2D)会更加高效且易于调节。

请按照以下步骤配置粒子系统:

  1. 添加节点:Bird 节点下新建一个子节点,搜索并选择 CPUParticles2D
  2. 配置基础发射参数: 在右侧属性面板中进行如下设置,以模拟向后飘散的效果:

    • 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),这样残影会被小鸟本体遮挡,视觉上更自然。
  3. 设置缩放曲线(逐渐变小):

    • 找到 Scale Amount Curve 属性,点击 New Curve
    • 在弹出的曲线编辑器中,将起始点(左侧)保持在 1.0,将结束点(右侧)拉低至 0.5。这表示残影在消失前会缩小到原来的一半。
  4. 设置透明渐变(逐渐消失):

    • 找到 Color Ramp 属性,点击 New Gradient
    • 在渐变色控制条中,点击左侧滑块确保颜色为纯白(Alpha 为 255)。
    • 点击右侧滑块,将颜色的 Alpha(透明度) 通道拉至 0。这会让残影产生从清晰到完全透明的过渡效果。
  5. 初始状态控制: 默认将 Emitting(发射) 属性设置为关闭。我们希望只有当小鸟飞起来时,才通过代码开启这个特效。

alt text 上图显示了设置缩放曲线完成后效果。

alt text 上图显示了设置透明渐变完成后效果。

4.1.5 设置音效节点

声音是游戏反馈的重要组成部分。我们需要为小鸟配置两个独立的音频通道,以便分别处理飞行和得分的声音。

  • 添加节点: 为根节点添加两个 AudioStreamPlayer2D 子节点。
  • 重命名: 分别命名为 FlySound(用于扑翼声)和 ScoreSound(用于得分奖励音)。
  • 说明: 你可以在属性面板的 Stream 选项中直接指定 .wav 文件。在本例中,我们暂时保持为空,稍后会在脚本逻辑中动态调用它们,以实现更灵活的控制。

4.1.6 设置节点分组(Group)

在游戏逻辑中,当碰撞发生时,管道或地面需要知道“是谁撞了我”。为了避免繁琐的路径引用,我们使用 Godot 的分组机制为小鸟打上一个身份标签。

  1. 进入分组面板: 选中 Bird 根节点,在编辑器右侧属性面板旁点击 Node(节点) 标签页,然后选择 Groups(分组)
  2. 添加标签: 在输入框中输入 bird,点击 Add(添加)

alt text

为什么要这样做? 当管道检测到碰撞时,它只需要检查对方是否在 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 制作基础管道组件

为了提高开发效率,我们先制作一个基础管道场景,后续通过旋转和位移将其变成上下两根管道。

  1. 创建场景: 新建场景,根节点选择 Area2D(用于检测碰撞),重命名为 Pipe
  2. 添加组件: 为其添加两个子节点:Sprite2D(视觉显示)和 CollisionShape2D(物理碰撞)。
  3. 调整精灵属性:
    • pipe.png 拖入 Sprite2D 的 Texture 属性。
    • 关键设置: 在属性面板中取消勾选 Centered。默认情况下图片的中心点在正中央,取消后轴心会位于左上角。
    • 轴心偏移:Offset.x 设置为 -39。这样做能让轴心(锚点)精准地定位在管道开口边缘的中心位置,极大方便了后续的对齐与旋转操作。
  4. 设置CollisionShape2D: 设置shape为RectangleShape2D,使用鼠标来操作矩形周边的小圆点(控制点),调整它的大小让它和上部管道的图形重合。完成后保存为 pipe.tscn

alt text 完成设置后的场景如上图所示。

4.2.2 组装完整障碍场景

现在,我们将刚才制作的单根管道组合在一起。

  1. 场景构建: 新建一个场景,根节点选择 Node2D,命名为 Pipes
  2. 嵌套子场景: 从文件系统中将 pipe.tscn 拖入当前场景两次,分别重命名为 PipeTop(上管道)和 PipeBottom(下管道)。
  3. 布局与对齐:
    • PipeTop(上管道):Position.y 设置为 90
    • PipeBottom(下管道):Position.y 设置为 -90,并将 Rotation 设置为 180 度进行翻转。
    • 这样处理后,上下两根管道之间正好留出了一个 180 像素 的空隙,供小鸟通过。将此场景保存为 pipes.tscn

4.2.3 添加金币与得分机制

金币不仅是分数的来源,其动感效果也能提升游戏品质。我们需要实现“常态旋转”和“获得后消失”两种动画。

  1. 构建金币节点: 在Pipes根节点根节点下添加 Area2D 子节点,命名为 Coin
  2. 给Coin添加两个子节点:
    • AnimatedSprite2D:用于显示金币旋转动画
    • CollisionShape2D:用于与小鸟检测是否通过得分
  3. 设置金币旋转动画:
    • 点击 Sprite Frames → 新建 SpriteFrames 资源
    • 将 coin-frame-1.png 到 coin-frame-11.png 拖入动画帧
    • 打开 Autoplay 和 Looping,让金币在一开始就自动播放并循环播放
    • 由于素材较大,将scale设置为0.3
    • 设置 FPS = 10
  4. 设置CollitionShape2D:使用 CircleShape2D,半径与缩放后的金币大小匹配即可。

alt text 完成设置后如上图所示

我们还需要添加金币收集后消失的动画,步骤如下:

  1. 给Coin添加子节点 AnimationPlayer
  2. 底部面板中会出现动画编辑区,点击Animation,增加一个名为coin的动画,动画时间设置为0.5秒。
  3. 点击Add Track添加动画轨道,选择Property Track,此时弹出对话框,选择AnimatedSprited2D,点击OK,再搜索scale属性,找到scale属性后选择Open。这样会创建一个动画轨道,它会控制金币的缩放比例。
  4. 后续的操作和第三章介绍的操作一样。在开始和结束时间建立两个关键帧。开始时间scale值为0.3,结束时间scale值为0.8。
  5. 类似的操作再增加一个Property Track,找到AnimatedSprited2D的modulate属性,开始时间的关键帧的颜色设置为白色,结束时间的关键帧的颜色设置为透明。

点击播放动画,你会看到硬币放大后淡出的效果。将时间轴还原到开始状态保存场景。

alt text 所有的设置完成后如上图所示。

4.2.4 屏幕外自动销毁

由于管道会源源不断地生成,如果不及时清理离开屏幕的管道,游戏运行一段时间后就会因为对象过多而变得卡顿。我们需要知道什么时候,Pipes场景离开了屏幕。一种方法是在代码中通过Pipes的position坐标来实现这个逻辑,另一种方法是使用一种特别的检测节点。操作步骤如下:

  1. 添加检测节点:给Pipes根节点增加一个子节点,类型为VisibleOnScreenNotifier2D。
  2. 节点作用:此节点有两个内置信号:
    • screen_entered:进入屏幕时触发
    • screen_exited:离开屏幕时触发(我们将用这个信号)
  3. 它能自动检测自身是否处于屏幕可见区域,非常适合判断“对象是否应被销毁”。你也可以在该节点上点右键,Open Document来阅读相关的帮助文档。

alt text 帮助文档如上图所示。

障碍场景的“骨架”和“特效”都已经搭建完毕,我们下面需要给它增加代码逻辑。

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.timeoutawait animation_player.animation_finished。这意味着:当前函数会暂停在这一行,直到信号被触发才继续往下执行。这种机制非常适合实现“延迟后执行某件事”的逻辑。在实际复杂项目中需注意原本的节点是否存在,如果在动画播放中节点被意外删除了,这个 await 可能会抛出异常。

(4)管道移动 最后,在 _process 每一帧的循环中更新管道的水平位置:

func _process(delta):
    # 根据速度和帧率间隔移动位置
    position.x += delta * SPEED

至此,我们完成了障碍场景的构建。这个模块不仅处理了视觉元素(上下管道、金币动画),也承担了核心逻辑(碰撞、得分、销毁)功能,是整个游戏玩法的关键环节。下一步继续实现 UI 场景,为玩家提供反馈界面与开始按钮。

4.3 HUD用户界面实现

在任何游戏中,用户界面(UI,User Interface)都扮演着向玩家传递信息与提供交互的关键角色。虽然《Flappy Bird》的 UI 风格极其简约,但麻雀虽小五脏俱全,它必须承担起显示实时得分、提示游戏状态(如欢迎语或游戏结束)以及提供“开始按钮”的任务。

在 Godot 中,所有的 UI 元素都属于 Control(控件) 类型节点。为了确保这些界面元素始终固定在屏幕上,不随摄像机的移动而位移,我们通常会将它们放置在独立的 CanvasLayer 渲染层中。

4.3.1 创建HUD场景

我们将利用 Godot 强大的“容器(Container)”系统来自动处理排版,这样无论玩家的屏幕分辨率如何变化,UI 都能保持美观。

  1. 创建根节点: 新建一个场景,根节点选择 CanvasLayer节点,并命名为 HUD。这个节点就像是一块透明的玻璃,盖在游戏世界(2D 空间)上方,专门负责渲染 UI。
  2. 设置安全边距:HUD 下添加 MarginContainer节点。它像一个相框,能防止文字紧贴屏幕边缘。
    • 选择MarginContainer节点,在设置中找到 Anchors Preset(锚点预设) ,选择 Full Rect(全矩形),让它铺满整个屏幕。
    • 在属性面板的 Theme Overrides > Constants 中,将 margin_top/bottom/left/right 全部设置为 20,预留出一定的间距。
  3. 自动垂直布局: 在 MarginContainer 下添加 VBoxContainer节点。这个容器非常聪明,它会自动将其下的所有子元素按照从上到下的顺序整齐排列,适合纵向堆叠 UI 元素。
  4. 最后将场景保存为hud.tscn文件。

4.3.2 添加UI控件元素

现在,我们将“填入”具体的控件。请在 VBoxContainer 下依次添加以下节点,并按照说明进行配置:

控件类型 重命名 用途说明
Label ScoreLabel 显示当前得分
Control 空白填充,调整布局间距
Label Message 显示欢迎提示或“Game Over”
Button 提供“开始游戏”操作
Control 底部填充

具体设置如下:

  1. ScoreLabel 分数显示设置
    • Text 设置为 "Score: 0",后续会通过代码来控制这里的属性,预先写入的内容是为了调试字体风格。
    • Horizontal Alignment = Center
    • Label Settings 是用于设置字体风格,点击New Label Setting新建样式资源,设置字体为LuckiestGuy-Regular.ttf,字号32,字色为白色,并添加阴影与描边,将这个样式保存为 white_32.tres 以方便后续复用。你会在文件系统中看到这个资源文件。如果这步操作有些困难,你也可以直接使用assets中已经制作好的资源文件。
  2. Message 提示信息设置
    • 初始 Text 设置为 "Welcome"
    • Label Settings处选择 quick load,选择使用 white_32.tres 样式资源
    • 设置 Horizontal Alignment = Center
  3. Button 开始按钮设置
    • Text 设置为 "Start Game"
    • Container Sizing > Horizontal 设置为 Shrink Center,让按钮宽度自适应
    • Theme Overrides设置其字体和大小,将字体文件拖入font,再将font size设置为20。
  4. Control 节点设置
    • 将两个 Control 节点的Container Sizing -> Vertical 设置为 Fill
    • 确保 Expand 勾选,表示自动占满可用空间
    • 作用:使按钮区域居中分布,整体布局美观

alt text 得分控件的设置界面如上图所示

alt text 完成设置后的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 像素的速度持续向左循环滚动。

alt text

这样我们的基础场景就完成了,保存为bg.tscn文件。

4.4.4 通过继承创建天空与地面

有了“模板”后,我们可以通过继承快速生成具体的场景。先来创建天空场景。

  1. 点击菜单 Scene > New Inherited Scene,开始新建继承场景。
  2. 在对话框中选择要继承的目标场景为bg.tscn,这样会创建一个新的子场景。你会看到新的场景节点是黄颜色的。
  3. 将根节点改名为Sky,保存这个场景,命名为sky.tscn。
  4. 在Sky场景中选择Sprite2D节点,将bg.png文件拖入Texture属性中,再把Offset属性中Centered的勾选取消掉,这样图片的左上角就会位于屏幕的原点位置。
  5. 设置ParallaxLayer节点的属性:
    • Motion > Scale 设置为 0.2,这意味着天空的实际滚动速度只有地面的 20%,视觉上会显得非常深远。
    • Motion > Mirroring 设置为 (864, 0),因为天空图片的尺寸是864*768,这样的设置是指当运动了864个像素后,再渲染一张新的图片。这样无缝循环可以实现无限背景的效果。

alt text 天空场景设置完成后如上图所示。

用同样的操作步骤,我们再继承创建地面子场景。

  1. 再次继承 bg.tscn,命名为 ground.tscn
  2. 将 Sprite2D 的贴图设置为 ground.png
  3. 取消 Centered,并将 Offset.Y = 700,让地面贴图显示在屏幕底部附近
  4. 设置 ParallaxLayer:
    • Motion > Scale 设为 1.0,表示地面速度与障碍一致
    • Motion > Mirroring 设置为 (900, 0),与地面图宽度一致

alt text 地面场景设置完成后如上图所示。

好了,通过这种基于继承的设计,我们不仅实现了极具层次感的视觉效果,还通过 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 来调用其中的信号和变量,就像调用内置函数一样方便。

alt text 完成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()

这样我们就完成了所有信号的传递,再归纳一下信号的使用方法。信号分为内置信号和自定义信号。内置信号只需要管发射和连接即可,自定义信号则需要三个步骤。

  1. 定义信号:在 GameManager 中使用 signal 关键字宣告事件。
  2. 连接信号:在 BirdHUD_ready 中使用 connect 连接关注事件,并绑定响应函数。
  3. 发射信号:在 PipesButton 交互时使用 emit 宣告事件发生。

这种模式让场景之间实现了“相忘于江湖,却感应于信号”的高级协作。现在,所有的零件都已经通过无线电联系在一起了,我们只需要最后一步:在主场景中把它们实例化出来!

6. 主场景整合与游戏流程控制

经过前面的构建,我们已经完成了所有核心模块:主角小鸟、障碍管道、用户界面、背景系统和全局管理器。现在,我们将把这些零散的场景组装成一个有机整体,并编写主逻辑来掌控游戏的生死循环。

6.1 搭建舞台:创建主场景结构

  1. 新建场景,根节点选择 Node2D,将其重命名为 Main
  2. 实例化子场景,点击节点面板上方的“实例化子场景”按钮(链条图标),依次将以下场景添加为 Main 的子节点:
    • sky.tscn(背景)
    • ground.tscn(地面)
    • bird.tscn(主角)
    • hud.tscn(用户界面)
  3. 调整空间层级与布局
节点名称 调整建议
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()
代码解释: 在new_pipes函数中,实例化Pipes场景,设置位置为一个横向值固定,纵向值随机。这样可以让管道出现的位置有一定随机性,最后将其添加到PipesGroup节点下。这个函数的作用,就等价于我们点击instantiate child scene按钮,在主场景中实例化Pipes场景。只不过是把人工操作换成了代码的自动操作。

6.6 运行与调试

现在,点击编辑器右上角的“运行当前场景”按钮(或按 F6)。我们可以开始游戏了。游戏场景如下。

alt text

通过以上结构,我们实现了完整的游戏流程:

  • 开始阶段,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 资源。可设置 autoplayloop 实现自动循环播放。
  • 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 游戏开发的基本流程,也初步建立了“模块化设计 + 脚本控制 + 信号通信”的开发思维模式。这将为你后续制作更复杂的游戏奠定扎实的基础。