跳转至

第十三章:视觉优化与发布

本章导言

一个好玩的游戏需要机制,一个令人难忘的游戏需要打磨。随着《Battle Tank》游戏的功能不断完善,我们已经完成了从地图生成、角色控制、敌人AI到战斗机制的完整流程。在本章中,我们将对游戏进行全面的优化与整合,为最终的发布做好准备。

我们首先从视觉表现入手,通过加入夜间效果、灯光、阴影和发光特效,增强游戏的氛围感和沉浸感。接着我们将优化用户界面,设计主菜单并实现场景切换,提升游戏的整体交互体验。同时,我们将构建一个全局的音效管理器,统一控制背景音乐和交互音效,使游戏音效更连贯。

为了实现多个关卡的支持,我们将新增一个关卡管理器,通过动态控制敌人波次与数量,实现游戏难度的递进,并在每个关卡开始前、胜利或失败后提供清晰的提示界面。随后,我们将调整游戏机制,让地图上的道路对玩家的移动产生实际影响,进一步丰富游戏玩法。

最后,我们还将完善游戏主场景的视觉配置,为不同关卡设置独特的色彩风格,并介绍如何将整个游戏打包发布,让你亲手制作的作品走向真实世界。

本章不仅是对前面章节的汇总提升,更是将一个“可玩原型”打磨成“完整游戏”的关键阶段。通过这些内容的学习与实践,你将掌握一套完整的游戏优化与发布流程,为未来的独立开发打下坚实基础。

1 光线和阴影

本节将为我们的游戏增添夜间效果与局部照明,让战场拥有更加鲜明的视觉氛围和沉浸感。这也是游戏视觉优化中最直观而有效的手段之一。

1.1 使用 CanvasModulate 节点创建夜间场景

在 Godot 的 2D 渲染系统中,CanvasModulate 是一个用于控制整个画布色调的特殊节点。它的作用类似于在整个屏幕上加了一层透明的彩色玻璃,通过调节其 color属性,可以对所有可见的 2D 元素施加一种统一的颜色滤镜。其典型用途包括:

  • 模拟昼夜变化:例如设置为深蓝或紫色可以模拟夜晚,设置为橘黄色可以模拟黄昏;
  • 烘托场景氛围:如火灾场景使用红色滤镜、森林场景用绿色调、迷雾场景用灰色调;
  • 实现过渡效果:配合动画控制颜色渐变,可制作场景之间的平滑过渡(如闪白、闪黑);
  • 营造统一画面风格:用于整个关卡统一色彩基调,提升视觉一致性。

CanvasModulate作用于整个渲染的 2D 画布,在节点树中的位置不影响其作用范围。无论它放在哪个节点下,只要存在,它就会对所有CanvasItem类型的节点(如 Sprite2D、Label等)生效。

但有一个例外:被放在 CanvasLayer节点下的内容会在独立画布中渲染,不会受到 CanvasModulate 的影响。这一特性非常实用,例如可以让 HUD 界面、爆炸特效等在夜间场景中仍保持清晰明亮。

下面按以下步骤来尝试创建一个夜间战斗场景:

  • 打开游戏主场景game.tscn
  • 给根节点增加一个新的子节点CanvasModulate
  • 设置颜色为深紫色(#2a2965),营造出夜色降临的氛围。

运行游戏,你会发现整体场景变暗,但 HUD 元素并未受影响。这是因为 CanvasLayer 层的内容会被独立渲染,不受 CanvasModulate 影响。我们会利用这一点来保留 HUD 的亮度,也会处理其它需要“例外处理”的元素。

1.2 Godot中的光照

在 Godot 的 2D 游戏开发中,光照系统为场景赋予了立体感、氛围感和动态变化,是打造沉浸式视觉体验的重要工具。Godot 提供了多种 2D 光源节点,主要包括:

  • 点光源(PointLight2D):它是最常用的 2D 光源类型,它会从自身所在的位置向四周发散光线,产生局部照明效果。适用于模拟角色手电筒、街灯、火把、爆炸闪光等具有明确“发光点”的场景。优点是效果直观,适用于大多数局部光源场景;缺点是性能消耗较高,建议在移动设备上适量使用。

  • 平行光照(DirectionalLight2D):它用于模拟从一个方向照射的均匀光线,适合构建具有统一光照方向的场景,比如日出、夕阳、月光照射等。它的特在于,并不会因位置而改变照明,而是按角度“扫过”整个画布;没有 Range 限制,影响整个场景;可以启用阴影投射(软阴影较为自然);常配合动态背景使用,例如光线穿透树林形成的剪影效果。适用场景为,构建宏观氛围,比如沙漠中的烈日、战场上的阴云密布。

  • 阴影与遮挡(LightOccluder2D):为了让光源在场景中投射出阴影,还需要配合使用 LightOccluder2D 节点,它负责定义哪些对象可以遮挡光线。节点提供一个遮挡多边形,当光照打在其上时,会生成动态阴影。你可以手动绘制遮挡形状,或在 TileSet 中定义Occlusion Layer。它适用于角色、障碍物、地形、建筑等实体。

1.3 增加点光源

夜间的游戏场景里面,需要增加灯光元素,来给游戏增加明暗的感觉。在这个游戏中我们只需要给两个单位增加灯光,一个是玩家控制的坦克,另一个是敌方炮塔,你也可以尝试在关卡其它位置增加灯光。操作步骤如下:

  • 打开player.tscn场景文件,在节点树中增加pointlight2D节点。
  • 将其texture属性设置为spotlight_1.png图片。这个图片是一个从中心向四周的渐变纹理,它将用于表现灯光的发光形式,就是中心更亮,周围更暗。
  • 然后将texture scale设置为5,让纹理更大,这样可以更好的表现出灯光的范围。
  • 修改Color为淡黄色。
  • Shadow设置为enabled,启用阴影投射。 具体的节点树和设置如下图所示:

alt text

重复上述步骤,我们给EnemyTower场景也增加pointlight2D节点,并完成设置。

再次运行游戏,夜幕中带有发光效果的单位将格外醒目,为战斗带来氛围感与美术张力。 alt text

1.4 处理被 CanvasModulate 影响的元素

在我们使用 CanvasModulate 节点创建夜间效果后,游戏整体氛围确实暗了下来。但你可能也注意到一个问题:某些特效元素,比如爆炸动画、子弹轨迹、拾取物品等,也一起被变暗了,导致它们失去了原本的高亮和冲击感,甚至变得不清晰、不自然。

这是因为 CanvasModulate 会作用于 默认渲染层 中的所有 2D 元素,统一叠加一层颜色滤镜。这虽然方便处理全局氛围,但也会影响到某些不该“变暗”的关键视觉效果。为了解决这个问题,我们有如下两种处理方法:

方法一:使用 CanvasLayer 隔离渲染

将不希望受到 CanvasModulate 影响的节点放入一个单独的 CanvasLayer 节点中。CanvasLayer 相当于一个“独立的画布”,其下的内容会在独立的渲染层中显示,不会被 CanvasModulate、全局光照等默认渲染逻辑影响。

你可能已经注意到 HUD 中的血条和计分板没有变暗,这是因为它们本身就放在了 CanvasLayer 中。这说明使用 CanvasLayer 隔离的方式是有效的,只不过它适合用于一整层的内容。

而爆炸、子弹等特效通常是动态实例化的,不太方便统一拖进 CanvasLayer 中。因此我们更推荐使用第二种方法,灵活且保持结构清晰。

方法二:设置CanvasItemMaterial

另一种更灵活的方法,是为这些节点添加一个 CanvasItemMaterial 材质,并将其 Unshaded 属性设置为 true。这样可以让节点“跳过”环境光照与颜色滤镜的影响,直接以自身原始颜色进行绘制。

Godot 的渲染流程中,节点默认会接受环境光、阴影、CanvasModulate 的颜色叠加等影响。勾选 Unshaded = true 后,节点会绕过这些全局处理阶段,直接用贴图本身的颜色进行渲染。这就像在舞台上关掉了聚光灯和滤镜,告诉 Godot:“这块画布我自己负责,不需要你来管。”

具体操作步骤如下:

  • 打开爆炸效果场景 exp_anim.tscn文件
  • 在根节点的右侧属性栏中,找到CanvasMaterial设置
  • 新建一个CanvasItemMaterial
  • 然后在light mode中设置为unshaed,使其不受任何光照与环境调色影响。

为保持统一风格,我们建议对下列场景也使用相同处理方式:

  • exp_particle.tscn(爆炸粒子特效)
  • bullet_base.tscn(子弹)
  • bullet_trail.tscn(子弹尾迹)
  • pick_up.tscn(道具)

注意你需要在场景中负责图片显示的节点中进行设置,如道具场景中的sprite2d节点。如果你有很多场景需要设置,可以将CanvasItemMaterial另存为一个资源文件,然后在需要设置的节点中引用它。

两种方法的对比如下表所示:

比较维度 方法一:CanvasLayer 方法二:Unshaded 材质
原理 独立渲染层,跳过全局 CanvasModulate 节点自行渲染,不受光照/滤镜影响
使用方式 将节点放入 CanvasLayer 给节点添加 CanvasItemMaterial 并设置属性
灵活性 较低,适用于整组 UI 更高,适用于单个精灵或粒子
常见用途 HUD、UI、提示框等 特效图片、子弹、光斑、发光物体
优势 清晰分组,统一控制 精细调节,不改变节点结构
劣势 结构变复杂,需要额外节点层 需要每个节点单独设置材质,略繁琐

1.5 设置阴影

到目前为止,我们为游戏添加了夜间环境和局部灯光,看起来已经很有氛围。但你可能会发现一个不协调的细节:虽然灯光照亮了场景,却没有投下任何阴影。例如,玩家坦克或石头障碍物被灯照射时,背后仍然是一片亮堂堂的,这显然不真实。要让光线产生遮挡并投下阴影,我们需要告诉引擎:“这些物体可以挡住光线。

我们可以通过两种方式来定义光线遮挡:

  • 给 TileMap 设置光线遮挡层或光线蒙版(适用于地图地形);
  • 在具体场景中添加 LightOccluder2D 节点(适用于动态角色或单位)。

下面我们分别来看这两种方式的设置方法。

第一种:为 TileMap 设置光线蒙版

我们先为地图上的石头添加光线遮挡,让它们能在灯光照射下投下阴影。操作步骤如下:

  • 打开地图网格资源文件 map.tres
  • 找到Rendering设置,在Occlusion Layers中点击add element,表示新建一个光线遮挡图层。
  • 在底部TileSet操作区中点击Paint标签
  • 在Paint Properties中找到Occlusion Layer 0
  • 选中要遮挡光线的网格单元,这里我们选择石头,点击选中三块石头,图块会盖上一层浅蓝色,这样就定义了光线遮挡层。
  • 默认的形状是正方形,和石头的形状不一样,可以通过Painting中的形状来调整阴影的形状,手动绘制更接近石头轮廓的遮挡多边形,再点击右侧的图块进行应用。石头有三种形状,所以需要设置三次。 设置光线遮挡的图示如下: alt text

完成后,保存 TileSet,运行游戏,你会发现灯光照到石头上时,背后会出现真实的阴影投射。 alt text

第二种:为动态单位添加 LightOccluder2D

地图是静态的,而玩家和敌人是动态的角色节点,它们也应该能投下阴影。这时候我们使用另一个专用节点:LightOccluder2D。我们需要给敌方两种单位场景定义光线遮挡。具体步骤如下:

  • 打开敌方坦克场景文件enemy_tank.tscn。
  • 在节点树中增加LightOccluder2D节点
  • Occluder属性中进行设置,新建一个多边形
  • 然后进入编辑模式,使用编辑工具来定义一个正方形,大约可以覆盖到敌方坦克的身体。

完成设置的图示如下:

alt text

使用类似的方法,也给敌方导弹攻击车定义光线蒙版。运行游戏后,你会看到敌方坦克在光照下也会投下动态阴影。当它移动时,阴影也会跟随它动态变化,增强了场景的真实感。

1.6 添加发光特效

虽然我们已经为游戏添加了灯光和阴影,但目前一些关键的视觉元素(比如坦克开火、爆炸动画)依然显得有些平淡。爆炸只是亮一点的图片,打中敌人时也没有什么特别的视觉冲击力。

要让这些特效不仅仅是图片变亮,而是像真正发光一样在屏幕上“闪耀”,我们就需要用到 Godot 的二项高级功能:发光特效(Glow Effect)和 环境特效(World Environment Effect)

理解Glow发光特效与HDR

首先我们需要理解HDR(高动态范围渲染),这个概念简单来说,就是让游戏画面里的“亮的地方能更亮,暗的地方也能更暗”。在普通模式下,颜色值的范围是 0 到 1,比如:黑色是 (0, 0, 0),白色是 (1, 1, 1)。而开启 HDR 之后,我们可以使用超过 1 的颜色值,例如:爆炸亮光可以设置为 (1.5, 1.3, 1.0)。这样就允许我们制造出“非常亮”的区域,为后面的发光效果提供条件。

Glow发光效果是一种画面后期特效,专门用来处理那些“太亮”的区域,让它们在屏幕上发出柔和的光晕。一旦你开启了 HDR,并让某个对象颜色超过了阈值,它就可以被 Glow 系统识别出来,并在物体周围自动添加发光边缘,就像爆炸的强光、技能的亮光、灯泡的光圈一样。Glow 是让“亮的东西不仅亮,而且发光”的关键。

HDR(High Dynamic Range,高动态范围渲染)是一种图形渲染技术,用来表现更接近真实世界的亮度范围。它允许画面同时呈现非常亮和非常暗的区域,并保留丰富的细节,而不会出现大面积过曝或死黑。通过 HDR,光源、阴影、反射和发光效果会更加自然真实,常用于增强游戏和影视画面的真实感与沉浸感。

什么是 WorldEnvironment ?

WorldEnvironment 是 Godot 中用于控制整个游戏世界视觉效果的节点,它允许我们在一个地方统一设置诸如背景、雾效、色调、发光(Glow)等后期处理效果,从而改变整个场景的视觉风格。

虽然它经常在 3D 游戏中使用,但在 2D 游戏中同样非常实用,特别是当你启用了 HDR 渲染 后,它可以用来设置和控制 Glow 发光特效、调整色彩饱和度、模拟雾气、控制画面对比度等视觉效果。

如果说 CanvasModulate 是为 2D 场景统一加一层颜色滤镜,那么 WorldEnvironment 就是为整个世界装上一个后期处理摄像机镜头,可以套用各种视觉特效。

常见功能一览如下:

功能区域 功能说明
Background 设置场景背景色或背景图(2D 中设置为 Canvas 模式)
Glow 启用发光效果,让颜色超亮区域在屏幕上产生光晕
Tonemap 控制画面对比度、曝光、亮度(用于统一色调)
Adjustments 微调色彩饱和度、对比度、色温(可用于制作冷色/暖色场景)
Fog 虽然主要用于 3D,但也可以用于某些 2D 场景的深度视觉模拟(例如雾霾效果)

设置 Glow 发光特效的步骤

为了让爆炸等特效真正“发光”,我们需要做三件事:

  • 第一步:在project setting中设置HDR2D,找到rendering下的Viewport,将HDR2D设置为true。这样会支持2D的发光特效。其设置图示如下:

alt text

  • 第二步:打开game.tscn主场景,新增WorldEnvironment节点,在设置中新建一个环境,将mode设置为Canvas,表示是用于 2D 渲染。在Glow设置中,将Enable开关设置为True,Blend设置为Screen,你也可以尝试其它模式。这里的HDR Thread是1,也就是说发光的阈值是1,只有当颜色设置值大于1的时候,才会有发光效果出现。其设置图示如下:

alt text

  • 设置需要发光的节点。打开爆炸场景文件exp_anim.tscn,找到Modulate设置,切换到RAW模式,设置RGB颜色值,将RGB所有的颜色值都设置为1.5,这样就超过了发光阈值1。

可以根据上面第三步的操作,将所有需要发光的节点都进行设置。保存后运行游戏,你会发现爆炸不仅更亮了,而且有了光晕外扩的闪光感!

如果你设置了颜色大于1却看不到发光,可参见如下可能的原因:

  • 项目设置中没有开启 HDR2D;
  • WorldEnvironment节点中有没有启用 Glow;
  • Glow 的阈值设得过高;
  • 需要发光的场景图片本身不够亮。

通过以上的优化,我们使用 CanvasModulate 营造夜间氛围,利用 PointLight2D 添加局部灯光效果,并通过设置 LightOccluder2DTileSet 遮挡层,实现真实的阴影投射。我们还通过启用 HDRGlow 特效,让爆炸等高亮元素具备发光表现,显著增强了游戏的视觉层次与沉浸感。

2 创建游戏主菜单

随着游戏功能的逐步完善,我们不仅需要让玩家“玩得起来”,还要让他们“玩得有结构”。一个游戏如果没有主菜单、关卡切换、胜利失败提示等流程引导,就像一本没有封面和目录的书,即使内容再精彩,也会显得零散而不完整。在本节中,我们将搭建一套基础的游戏流程管理系统,包含:

  • 制作主菜单界面,提供“开始游戏”和“退出游戏”选项;
  • 实现场景之间的切换,如从菜单跳转到游戏主场景;
  • 建立全局关卡管理逻辑,控制每一关的进入、胜利、失败与进度推进;
  • 添加按钮样式、文字样式和交互音效,提升界面体验感;
  • 引入 SoundManager 音效管理器,统一控制背景音乐和按钮音效;

2.1 主菜单开发

一个良好的主菜单不仅是游戏的“门面”,更是连接玩家与游戏世界的第一道桥梁。本节我们将为游戏设计一个具有完整布局、美术风格统一、可交互的主菜单界面,并做好跳转逻辑的准备。

创建主菜单场景

新建一个场景,根节点为Control类型,重命名为Menu。我们希望菜单画面中显示一张背景图,作为游戏风格的初步展示。操作步骤如下:

  • 在根节点下增加一个TextureRect节点。该节点类似于Sprite节点,用于在用户界面中显示背景图片。
  • 在文件系统中找到tank_menu.png,将其拖拽到Texture属性中。
  • 将其 Anchors Preset 设置为 Full Rect,让它充满整个屏幕。

在背景图中,一辆红色卡通风格的坦克位于左侧,右侧留有空白,非常适合放置标题和按钮。下面继续在右侧来放置标题和按钮。

创建标题和按钮

为了让标题和按钮在右侧整齐排列,我们使用会使用布局容器节点,这些容器节点可以自动调整子节点的位置和尺寸。操作步骤如下:

  • 在TextureRect节点下新增一个子节点,类型为MarginContainer。它专门用于控制子节点的内边距。
  • Anchors Preset设置为Right Wide,这样它会紧贴窗口布局的右侧。
  • size设置为(500,800)position设置为(780,0)
  • Theme Overrides中Constants设置为(20,20,20,20),让内边距为20,让子节点不贴边。

游戏标题和两个按钮会纵向排列,所以我们需要VBoxContainer。这个容器节点专门用于纵向排列子节点。操作步骤如下:

  • 在MarginContainer下新增一个子节点,类型为VBoxContainer
  • Container SizingVertical设置为Shrink Center,表示子节点会自动缩小居中以适应父节点的尺寸。
  • Theme Overrides中Constants设置为30。这样各元素之间的间距就是30个像素。

继续在VBoxContainer下新增四个子节点,分别是:

  • Label:用于显示游戏标题。
  • Contol:用于空间占位,让按钮整体下移
  • 第一个PanelContainer,用于放置“开始游戏”按钮。在其下新增子节点,类型为Button。
  • 用上面同样的作法,放置第二个PanelContainer及其Button。用于放置“退出游戏”按钮。

之所以使用PanelContainer的原因在于,Button控件本身样式控制能力是有限的。如果想让按钮有一种统一的“外壳风格”,那就需要额外的容器来提供这种视觉样式。

设置标题和按钮样式

继续来设置标题 Label 的样式:

  • Text设置为"Battle Tanks",这是游戏的名字。
  • Horizontal Alignment设置为Center,让它水平居中;
  • Vertical Alignment设置为Center,让它垂直居中;
  • Label Settings中来设置字体,字号,颜色。字体设置为Rockboxcond12size设置为100,color设置为红色。此外还可以给文字加上描边和阴影。

接着来美化容器和按钮,设置PanelContainer,使其更有立体感和风格统一性:

  • Custom Minimum Size设置为(200,0)
  • Theme Overrides中将Styles选择Quick load,选择之前设置过的资源文件panel.tres。这样所有的面板都会使用相同的风格。

对于容器中的两个Button以如下参数设置:

  • Text分别设置为"Start Game"和"Quit Game"。
  • Theme Overrides中设置Font Hover Color为红色,Fone Color为白色。这样鼠标经过按钮时,按钮的颜色会变成红色,鼠标离开时会变成白色。然后在Fonts处设置同样的字体,并在Font Sizes设置大小为30。

这样就构建了一个具有良好布局、统一美术风格的主菜单界面。最终菜单界面如图所示: alt text

2.2 修改全局管理器

在一个结构良好的游戏中,场景跳转、分数管理、关卡进度等全局功能不应分散在各处处理,而是统一交由一个“全局管理器”来协调。

我们在前几章中已经使用过一个脚本节点 gamemanager.gd。它是一个设置为 Autoload 的脚本,可以在任意场景中访问,是处理游戏流程逻辑的最佳位置。本节我们将继续完善它,让它支持玩家在主菜单与游戏场景之间切换等重要功能。

打开gamemanager.gd脚本文件,加入菜单跳转的逻辑。增加代码如下:

signal level_start

@onready var game_scene: PackedScene = preload("res://Scenes/game/game.tscn")
@onready var menu_scene: PackedScene = preload("res://Scenes/UI/menu.tscn")

var current_level:int = 1
代码解释:

  • 定义了一个新的信号level_start,表示“进入关卡开始游戏”。我们将在其他脚本中监听这个信号,启动关卡逻辑;
  • game_scenemenu_scene:分别加载游戏主场景和主菜单场景的资源。我们使用 preload() 提前加载,确保切换时速度快、不卡顿;
  • current_level变量表示当前关卡编号,默认为第1关,后续可以根据它动态控制敌人数量、地图颜色等。

func reset_score():
    score = 0

func next_level():
    current_level += 1
代码解释:

  • reset_score函数用于重置分数,,常在进入新一关或重新开始游戏时调用;
  • next_level函数用于将当前关卡号加一,用于关卡推进逻辑,比如击败所有敌人后调用。

func to_menu():
    get_tree().change_scene_to_packed(menu_scene)

func to_game():
    get_tree().change_scene_to_packed(game_scene)
代码解释:

  • to_menu函数用于跳转到主菜单
  • to_game函数用于跳转到游戏场景
  • 内置函数get_tree会返回的是当前运行游戏的场景树SceneTree对象,它是 Godot 引擎中管理游戏中所有节点的核心对象。
  • 内置函数change_scene_to_packed的作用是,将当前运行的场景替换为你提供的 PackedScene 实例,也就是说,它会先卸载当前场景(包括其所有节点和资源),然后加载并运行你指定的新场景。

这样我们给全局管理器增加了多个函数功能,后续我们将在其它脚本中调用这些函数,实现从主菜单进入游戏的交互操作。

2.3 开发音效管理器

在游戏开发中,声音效果与背景音乐是不可或缺的元素,它们不仅增强游戏的氛围,也能为玩家提供及时反馈(如点击确认、敌人爆炸等)。在我们早期的开发中,常采用“就近播放”的方式:在哪个场景需要声音,就在该场景中添加 AudioStreamPlayer 节点。这种方法简单直接,但在实际项目中会遇到一个关键问题:如何让背景音乐在多个场景间保持连续播放?

举个例子:你在主菜单中播放了一段背景音乐,但当玩家进入游戏场景时,菜单场景被卸载,音乐也随之停止了。这并不是我们希望的行为。

解决这个问题的最佳方式就是创建一个音效管理器(SoundManager),将其设置为 Autoload(自动加载单例)。它将在游戏运行的整个生命周期中常驻,可以在任何场景中播放音效和音乐,不会随着场景切换而消失。

新建一个场景,根节点设置为Node类型。重命名为SoundManager。在根节点下新增两个AudioStreamPlayer节点,分别命名为Music和Sound。这两个节点分别用于播放背景音乐和其他音效。在文件系统中找到BG.wav文件,将其拖拽到Music节点Stream属性中。

给SoundManager节点设置脚本,代码如下:

extends Node

const hover_sound = preload("res://assets/sound/scifi_ui_beep_button_06.ogg")
const click_sound = preload("res://assets/sound/scifi_ui_confirm_upgrade_14.ogg")
@onready var music: AudioStreamPlayer = $Music
@onready var sound: AudioStreamPlayer = $Sound
代码解释:

  • 使用preload函数加载两个音频文件,分别表示鼠标点击和鼠标滑过的声音;
  • 然后定义两个节点引用,分别是MusicSound,分别用于播放背景音乐和其他音效。

func play_music():
    music.play()

func stop_music():
    music.stop()

func change_music_vol(value):
    music.volume_db = value
代码解释:

  • play_music函数用于播放背景音乐
  • stop_music函数用于停止背景音乐
  • change_music_vol函数用于调整背景音乐的音量,单位为分贝。

func play_click():
    sound.stream = click_sound
    sound.play()

func play_hover():
    sound.stream = hover_sound
    sound.play()
代码解释:

  • play_click函数用于播放鼠标点击的音效
  • play_hover函数用于播放鼠标滑过的音效。
  • 每次播放前将对应的音频流设置给 sound 播放器,然后调用 play函数播放

最后为了让 SoundManager 在所有场景中都能使用,我们需要将其设置为 Autoload。打开 Project Settings > Autoload 标签,添加sound_manager.tscn 场景,名称为 SoundManager。设置完成后如下图所示: alt text

2.4 为主菜单添加脚本

在前面章节中,我们已经完成了主菜单的界面设计,我们接下来要为其添加脚本代码,让玩家点击按钮时能够启动游戏或退出程序。同时,我们也要为按钮加入交互音效,例如鼠标滑过时播放提示音、点击时播放确认音。这些细节将让整个菜单更具互动感和专业性。

给menu场景根节点挂载脚本,代码如下:

extends Control

@onready var start_button: Button = $TextureRect/MarginContainer/VBoxContainer/PanelContainer/StartButton
@onready var quit_button: Button = $TextureRect/MarginContainer/VBoxContainer/PanelContainer2/QuitButton
代码解释:定义两个节点引用变量,分别是开始游戏按钮和退出游戏按钮。然后在_ready函数中添加如下代码:

func _ready():
    start_button.pressed.connect(on_start_pressed)
    quit_button.pressed.connect(on_quit_pressed)
    start_button.mouse_entered.connect(on_mouse_enter)
    quit_button.mouse_entered.connect(on_mouse_enter)
    SoundManager.play_music()
    SoundManager.change_music_vol(0)
代码解释:

  • _ready函数中将两个按钮的pressed信号进行关联,当按钮被点击时,会触发响应函数。
  • 同时将mouse_entered信号关联到响应函数,当鼠标滑过按钮时,会触发响应函数。
  • 最后会播放背景音乐和调整音量。

func on_start_pressed():
    SoundManager.change_music_vol(-20)
    SoundManager.play_click()
    Gamemanager.to_game()
代码解释:on_start_pressed函数负责响应开始游戏按钮的点击事件,首先会调整背景音乐的音量,然后播放点击音效,然后使用Gamemanager的to_game函数切换到游戏场景。

func on_quit_pressed():
    SoundManager.play_click()
    await SoundManager.sound.finished
    get_tree().quit()

func on_mouse_enter():
    SoundManager.play_hover()
代码解释:

  • on_quit_pressed函数负责响应退出游戏按钮的点击事件,首先播放点击音效,然后等待音效播放完成后,使用get_tree().quit函数退出游戏。
  • on_mouse_enter函数负责响应鼠标滑过按钮的事件,它只需要调用SoundManager.play_hover函数即可。

这些逻辑将主菜单从“静态界面”转化为“交互体验”,为玩家营造更真实、更有沉浸感的第一印象。

3 关卡管理和UI优化

在之前的版本中,我们的游戏只有一个固定关卡,玩家通关后无法继续挑战新的内容。这种设计虽然适合原型开发,但显然限制了游戏的扩展性与耐玩度。

本节将引导你为游戏加入多关卡支持,并通过构建一个专门的关卡管理器(LevelManager)来统筹处理每一关的初始化、提示动画、胜利与失败后的过场 UI,以及关卡切换逻辑。与此同时,我们还将对敌人生成系统进行优化,使得随着关卡的推进,敌人的数量与波次逐渐增加,从而带来更富层次感的挑战。

3.1 修改敌人生成逻辑

首先,我们需要修改EnemyManager对应代码逻辑。原来的敌人波次和每波敌人数是固定的,但现在我们希望其数量能够随关卡递增。打开enemy_manager.gd文件,在合适位置添加如下变量定义:

var wave_number: int = Gamemanager.current_level + 1
var enemy_in_wave: int = Gamemanager.current_level + 3
代码解释:

  • wave_number表示当前关卡应包含的敌人波次,随着关卡数递增。
  • enemy_in_wave表示每一波敌人包含的数量,同样随关卡递增。

func _ready() -> void:
    tower_points = map.get_tower_points(enemy_tower_num)
    Gamemanager.entity_died.connect(on_entity_died)
    Gamemanager.level_start.connect(on_level_start)
    spawn_tower()
    check_enemy_size()


func on_level_start():
    wave_number = Gamemanager.current_level + 1
    enemy_in_wave = Gamemanager.current_level + 3
    spawn_waves()
代码解释:

  • _ready函数中增加对level_start信号的监听
  • 当关卡开始时会根据关卡编号修改wave_numberenemy_in_wave变量,这些变量基于关卡编号计算,例如第一关时有两个波次,每个波次有3个敌人,第二关会有三个波次,每个波次有4个敌人,以此类推。
  • 然后调用spawn_waves函数来生成相应数量的敌人。

func get_enemy_data():
    var data :Dictionary
    data['wave'] = wave_number
    data['enemy'] = total_enemy_size
    return data
代码解释:get_enemy_data函数负责返回一个字典数据,包含当前关卡的波次和敌人数量。此数据会提供给UI元素显示使用。

3.2 创建关卡管理器场景

在关卡逻辑逐渐丰富之后,仅靠单个脚本控制所有流程已不再高效。我们需要一个独立场景来集中处理关卡开始、暂停、胜利、失败、界面提示与切换等流程。我们需要一个专门的关卡管理器(LevelManager),它的作用可以理解为“关卡导演”:

  • 它不参与具体战斗,但统筹全局流程;
  • 管理 UI 面板的显示与隐藏;
  • 控制游戏暂停与继续;
  • 响应胜负并决定后续操作。

场景结构设置

新建一个场景,根节点设置为Node,重命名为LevelManager。其主要结构如下:

LevelManager (Node)
├── CanvasLayer
│   └── Control
│       ├── ContinuePanel  (开场提示)
│       ├── WinPanel       (胜利界面)
│       └── LossPanel      (失败界面)
└── Timer                  (延迟显示 UI)

在根节点下添加一个类型为CanvasLayer的子节点,允许 UI 元素在屏幕上单独渲染。CanvasLayer 负责将 UI 独立渲染在前景,但它不负责布局和组织内容。在CanvasLayer节点下添加一个类型为Control的子节点。Control 则专门用于 UI 组件的组织管理,管理子节点 UI 元素的大小、位置、是否显示以及响应输入等行为。

在Control子节点下放置三个PanelContainer,分别重命名为ContinuePanel,WinPanel,LossPanel。PanelContainer 是带有皮肤的 UI 容器,自带边框和背景,适合用于展示提示框、菜单、对话框等。

这三个面板容器的作用不同:ContinuePanel用于当玩家进入关卡后显示提示信息,玩家点击其中的确认按键后开始游戏。WinPanel用于显示当前关卡胜利后的提示信息,玩家确认后跳转到下一关。LossPanel用于显示当前关卡失败后的提示信息,玩家确认后返回主菜单。所以这三个容器都需要加入一些文本显示和按钮等UI元素。

ContinuePanel节点的参数设置如下:

  • CustomMinimumSize: (600, 170)
  • Anchors Preset: Center Bottom,它会位于底部居中位置。
  • Theme Overrides: Styles设置为panel.tres

ContinuePanel下放置子节点VBoxContainer,它是垂直布局容器,其下再放置三个子节点,分别为RichTextLabel、Button和AnimationPlayer。这三个节点的设置如下:

RichTextLabel节点用于格式化显示提示文字,具体设置为:

  • BBCode Enabled设置为true,BBCode是一种用于格式化文本的语法,可以在文本中插入样式和格式,比如加粗。
  • Displayed Text中的Visible Characters设置为0,Visible Ratio设置为0.0。这两个设置是控制文本的显示字数和显示比例,将这两个值设置为0可以让文本完全隐藏,这样一开始不会显示提示信息。后续我们会用代码控制Visible Ratio属性,让文本逐渐显示,呈现一个打字机的动画效果。
  • Container Sizing中将Vertical设置为Fill,Expand设置为勾选。
  • 最后适当修改一下主题中的字体,让其风格与其它字体一致即可。

ContinuePanel下的Button用于让玩家点击以开始游戏,设置如下:

  • Text设置为"Continue"
  • CustomMinimumSize: (120, 40)
  • Container Sizing设置为Shrink Center
  • 最后需要将其隐藏起来,等到文本显示完毕再显示出来。

AnimationPlayer节点用于控制文本显示动画。设置如下:

  • 新建一个动画命名为show,时长为1秒。
  • 添加一个属性轨道,对应的属性是Label的visible_ratio
  • 在第0秒的值设置为0,在第1秒设置为1。

继续来设置用于胜利提示的面板WinPanel,设置如下:

  • CustomMinimumSize: (400, 150)
  • Anchors Preset: Center Bottom
  • Theme Overrides: Styles设置为panel.tres

这样它会显示在屏幕的中间,在其下放置一个VBoxContainer,VBoxContainer下再放置两个子节点,分别为Label和Button。

按如下要求设置Label:

  • Text设置为"You Win"
  • Label Settings设置为Font设置为font35.tres,这是我们之前保存过的字体资源文件。
  • Alignment设置为Center
  • Contain Sizing设置为Vertical Fill,选择Expand。

按如下要求设置Button:

  • Text设置为"Next Level"
  • Contain Sizing设置为Vertical Fill,选择Expand。

最后需要设置LossPanel。LossPanel的设置和WinPanel类似,其中的文本显示为"You Lose",Button的文本显示为"Back Menu"。

所有的UI元素都设置好后,需要在LevelManager根节点后再增加一个Timer节点,Timer 用于控制胜利或失败后延迟几秒再显示结果界面,防止游戏信号切换过于突兀。

整体的节点树应该如下所示: alt text

场景代码逻辑

下面来给LevelManager添加代码,代码如下:

extends Node

@export var enemy_manager: Node
@export  var map: Node2D

@onready var label: RichTextLabel = $CanvasLayer/Control/ContinuePanel/VBoxContainer/Label
@onready var animation_player: AnimationPlayer = $CanvasLayer/Control/ContinuePanel/VBoxContainer/AnimationPlayer
@onready var continue_panel: PanelContainer = $CanvasLayer/Control/ContinuePanel

@onready var continue_button: Button = $CanvasLayer/Control/ContinuePanel/VBoxContainer/ContinueButton
@onready var control: Control = $CanvasLayer/Control
@onready var timer: Timer = $Timer
@onready var win_panel: PanelContainer = $CanvasLayer/Control/WinPanel
@onready var loss_panel: PanelContainer = $CanvasLayer/Control/LossPanel
@onready var win_button: Button = $CanvasLayer/Control/WinPanel/VBoxContainer/WinButton
@onready var loss_button: Button = $CanvasLayer/Control/LossPanel/VBoxContainer/LossButton
以上代码均是对变量的定义,这里不再赘述。

func _ready() -> void:

    continue_button.pressed.connect(on_continue_pressed)
    win_button.pressed.connect(on_win_pressed)
    loss_button.pressed.connect(on_loss_pressed)
    Gamemanager.player_killed.connect(on_player_killed)
    Gamemanager.player_win.connect(on_player_win)
    pause_level()
    intro_anim()
代码解释:

  • _ready函数中先绑定若干信号
  • 调用pause_level函数用于让游戏暂停
  • 调用intro_anim函数用于播放开场动画,这两个函数具体实现会在下面分别讲到。

func start_level():
    get_tree().paused = false

func pause_level():
    get_tree().paused = true
代码解释:

  • get_tree内置函数会返回管理当前游戏的场景树对象,它的paused属性用于控制游戏是否暂停,如果值为true,那么所有的输入事件,节点更新都会被暂停。
  • 分别定义了start_levelpause_level函数,用于控制游戏的开始和暂停。

func intro_anim():
    control.show()
    continue_panel.show()
    win_panel.hide()
    loss_panel.hide()
    set_intro()
    continue_button.hide()
    animation_player.play("show")
    await animation_player.animation_finished
    continue_button.show()
代码解释:

  • intro_anim函数负责播放开场动画
  • 首先将ContinuePanel显示出来,隐藏不需要显示的面板。
  • 然后使用set_intro函数设置文本内容,隐藏Continue按钮
  • 然后播放动画,等待动画播放完成后,再显示Continue按钮。

func set_intro():
    var data = enemy_manager.get_enemy_data()
    var text = "[center]Level %s[/center] \n There are %s attack waves and %s enemies." %[Gamemanager.current_level, data['wave'], data['enemy']]
    text +="\n kill them all and survive!"
    label.text = text 
代码解释:

  • 因为每一关的敌人数量不一样,显示的数据也不一样。所以通过set_intro函数负责设置文本内容
  • 通过enemy_manager获取敌人数据,然后拼接成文本
  • 这里使用了BBCode语法,让文本支持居中,最后将文本设置给RichTextLabel。

func on_continue_pressed():
    Gamemanager.level_start.emit()
    continue_panel.hide()
    control.hide()
    start_level()
代码解释:

  • on_continue_pressed函数负责处理Continue按钮的点击事件,它会触发Gamemanager的level_start信号
  • level_start信号会通知enemy_manager开始生成敌人
  • 然后隐藏相关的UI元素
  • 运行start_level函数,开始游戏。

func on_player_killed():
    timer.start()
    await timer.timeout
    control.show()
    loss_panel.show()

func on_player_win():
    timer.start()
    await timer.timeout
    control.show()
    win_panel.show()
代码解释:

  • on_player_killedon_player_win函数负责处理玩家死亡和胜利的情况
  • 它们会触发Timer开始计时,然后等待计时结束,再显示相关的UI面板。

func next_level():
    Gamemanager.next_level()
    Gamemanager.reset_score()
    get_tree().reload_current_scene()

func on_loss_pressed():
    Gamemanager.to_menu()
代码解释:

  • next_level函数负责加载下一关,调用gamanager的两个函数进行全局数据更新
  • 然后使用reload_current_scene重新加载当前场景。
  • 因为游戏关卡是自动生成的,所以重载后会生成一个新的随机关卡。
  • on_loss_pressed函数负责返回主菜单。

最后很重要的一项操作,需要给LevelManager节点的Process Mode设置为Always,意味着即使当 get_tree().paused = true(游戏暂停)时,该节点仍然会继续正常运行。对于关卡管理器进行这项设置,是为了确保即使在游戏暂停时,它依然可以控制动画、定时器和 UI 响应,保证关卡流程和交互体验正常进行。

3.3 HUD代码简化

由于胜利和失败的 UI功能 都由 LevelManager 统一管理,我们可以将 HUD 中原本用于胜利提示或失败提示的节点和代码删除,只保留显示玩家状态(如分数、血量)的部分。重构后的 HUD 更加专注,只负责实时信息的展示,职责更加单一清晰。

具体的代码可以参考本书附带的代码库。简化后的HUD节点树如下所示:

alt text

这样我们就完成了管理关卡与 UI 优化。通过改造 EnemyManager 支持动态波次与敌人数生成。构建丰富的过场体验,包括打字动画、胜利失败提示界面。简化 HUD 场景结构,使职责更清晰。实现关卡之间的切换机制,提升游戏可玩性。

4 修改游戏机制

在当前版本的游戏中,地图虽然生成了草地与道路,但玩家控制的坦克在任何地形上移动速度都是一样的。这让道路的存在显得缺乏实际意义,也削弱了地图设计的策略性。

为了增加游戏的真实感与可玩性,我们将调整移动逻辑,使坦克在 道路上移动更快,而在 草地上则稍慢一些。这样,玩家在行驶路线选择上将有更多思考空间,同时地图也会更具功能性。具体思路包括两个步骤:首先判断某个坐标点是否在道路上,其次是让玩家坦克在道路上移动更快。

打开map_pcg.gd代码文件,增加代码如下:

func is_on_road(pos:Vector2):
    var cell = road.local_to_map(pos)
    if cell in road_cells:
        return true
    else:
        return false
代码解释:

  • is_on_road函数用于判断某一个坐标是否在道路上,如果在道路上返回true,否则返回false。
  • local_to_map内置函数将全局坐标转换为地图格子坐标。
  • road_cells是一个数组,记录了所有生成的道路格子。

接下来,我们修改玩家脚本,使其在检测到“当前处于道路上”时提升移动速度。打开player.gd文件,增加代码如下:

@export var map: Node2D

func _physics_process(delta: float) -> void:
    if map.is_on_road(global_position):
        max_speed = 250
    else:
        max_speed = 150
    move(delta)
    if can_shake:
        shake()
代码解释:

  • map变量用于引用map节点
  • 修改_physics_process函数,调用is_on_road函数来判断坦克是否在道路上,如果在道路上max_speed会设置为250,否则为150。这段代码就完成了坦克在道路上移动更快的效果。

完成以上修改后,运行游戏并操作玩家坦克。你将发现:在灰色道路上行驶时,坦克移动速度更快,反馈更加流畅;当驶入绿色草地区域时,速度降低,动作更显沉重;

通过这一改动,我们让地图地形从“纯装饰”变成了“功能组件”,玩家需要根据地形特性做出选择,提升了游戏的深度与可玩性。

5 修改游戏主场景

完成关卡管理器和地形机制调整后,我们将回到游戏的主舞台Game.tscn,进一步优化游戏体验。本节的目标有两个:

  • 为不同关卡设置独特的视觉氛围色调,模拟不同时段的环境变化(如黎明、正午、黄昏、夜晚);
  • 在场景中集成关卡管理器 LevelManager,确保游戏运行流程完整,支持开场动画、胜败判断与关卡切换。

设置关卡氛围色彩

我们希望在不同的关卡有不同的色彩体验,我们会提供五种基本的颜色,会模拟不同时间的氛围。这五种颜色分别是:

场景名称 色调 HEX
清晨蓝 #6696ba
正午黄 #e3e38b
傍晚橙 #e8a453
日落紫 #7e4c69
深夜蓝 #2a2965

打开 Game.tscn 文件。选中根节点下的 CanvasModulate 节点。给节点附加代码如下:

extends CanvasModulate

@export var map_colors : Array[Color]

func _ready() -> void:
    var index = Gamemanager.current_level - 1
    if index >= map_colors.size():
        index = Gamemanager.current_level % map_colors.size() - 1
    color = map_colors[index]
代码解释:

  • map_colors变量用于存储颜色数组,并且用@export属性标记为可编辑
  • 然后在_ready函数中取得当前关卡的编号,根据当前关卡编号 current_level 来选择相应的颜色
  • 如果编号超过了六种颜色的数量,那么就取余数进行计算,这样就能够循环使用数组中的颜色,避免数组越界。

编辑器中回到Game.tscn场景,点击CanvasModulate节点的map_colors属性,选择add element,逐个添加列表中的五种颜色。你也可以根据自己的美术风格设计其他色调,营造独特的视觉感受。

集成 LevelManager 节点

为了让关卡管理功能正常运作,我们需要将 LevelManager 节点加入 Game.tscn 主场景中。操作步骤:

  • 打开 Game.tscn场景文件,将 LevelManager场景放置在节点树中。
  • 选中 LevelManager 节点,确认Process Mode 已经设置为 Always,确保在游戏暂停状态下仍能运行 UI 动画和按钮交互。

设置完成后,主场景最终的节点树如下所示: alt text

通过本节改动,我们可以让游戏主场景支持根据关卡切换不同的视觉氛围,提高游戏沉浸感;成功集成了关卡管理逻辑,实现开场提示、胜利切关、失败回退等完整流程。

修改鼠标样式

在默认情况下,Godot 游戏会使用操作系统的标准鼠标指针。但在本游戏中,为了提升沉浸感,我们可以将鼠标样式替换为自定义图像,例如变成一把剑、一只手或一个瞄准器。

下面是一种替换鼠标样式的简单方法,通过项目设置来完成。这种方法简单直观。打开项目设置,依次点击菜单:

Project > Project Settings > Display > Mouse Cursor

在这里你会看到几个与鼠标相关的设置选项。

  • Custom Image,用于设置鼠标的自定义图像。找到资源中的图片文件 mouse_target.png,这个图片将会作为游戏中默认使用的鼠标样式。
  • Custom Image Hotspot,这个设置用于指定鼠标图像的“点击热点”位置。默认情况下,Godot 会以图像的左上角 (0, 0) 作为点击位置。我们使用的图像是一个准星,图像大小是64x64,所以需要设置为图像中心,将其设置为 (32, 32)。

关闭 Project Settings,运行游戏后,你会发现鼠标样式已经变成你设置的自定义图像了。使用 Project Settings 设置的是全局默认样式,会作用于整个游戏。如果你希望在某些界面临时更换鼠标样式(如瞄准器、拖拽图标等),那还是推荐使用代码来动态切换。

6 游戏发布

游戏开发不只是“做出来”,还要能“交付出去”。发布游戏的过程,就是将你的项目打包成一个普通用户可以运行的独立程序的过程,不需要安装 Godot,也不需要源代码。

下面我们以 Windows 平台为例,将我们开发的《Battle Tank》游戏打包发布为 Windows 的可执行文件(.exe),让朋友、家人,甚至全球玩家都能体验你的作品。

6.1 打包前的准备工作

在开始导出之前,请先完成以下检查,避免在打包过程中出现问题:

  • 确认主场景(Main Scene)已设置:打开 Project > Project Settings > Application > Run,确保 Main Scene 设置为你要运行的主游戏场景,在我们的游戏中,游戏入口是主菜单,也就是res://Scenes/UI/menu.tscn

  • 保存所有文件:确保项目中的所有场景、脚本、资源都已保存,并且没有报错。

  • 测试运行无误:点击“播放”按钮,从主菜单开始完整游玩一次,确认不存在逻辑错误、资源路径丢失或运行异常。

6.2 设置导出模板

Godot 引擎本身并不包含导出功能,首次导出需要安装导出模板(Export Templates):

  1. 点击菜单栏 Editor > Manage Export Templates。进入导出模板管理器。
  2. 点击“Download and Install”按键,系统会自动连接到 Godot 的服务器,下载安装对应版本的导出模板。
  3. 安装完成后即可支持打包发布各种平台。

如果这种方式下载很慢,可以直接到官网找到和你的 Godot 版本对应的模板下载。然后仍然是从上述菜单中点击“Install from File”即可。

6.3 创建 Windows 导出配置

导出模板安装完成后,就可以开始配置 Windows 平台的导出选项。

  1. 进入菜单栏 Project > Export。
  2. 点击左下角 Add 按钮,选择 Windows Desktop。
  3. 这会在左侧添加一个“Windows Desktop”的导出设置项。

主要配置项说明:

  • 名称(Name):是指导出配置的名字而非导出可执行文件的名字,使用默认的“Windows Desktop”即可。

  • Export Path(导出路径):这里设置导出可执行文件的名字以及保存路径,你可以在计算机中专门创建一个名为“ExportGame”的目录,然后选择这个目录,例如ExportGame/MyTankGame.exe,这表示导出的 .exe 文件会保存在此文件夹中,可执行文件名为MyTankGame.exe

  • Architecture(架构):你可以根据玩家使用的计算机架构来选择 32位 或 64位,默认为 x86_64,如果是ARM架构的计算机,可以选择 arm64。

  • Icon(图标):你可以上传一个图标,让生成的 .exe 文件看起来更美观。

  • Embed PCK(包含资源包):勾选“Embed PCK”表示将游戏资源打包进 .exe 中(推荐方式),否则会生成一个 .exe.pck 两个文件。

6.4 导出并运行游戏

完成配置后,即可正式导出游戏:

  1. 点击窗口下方的 Export Project 按钮;
  2. 在弹出的对话框中确认导出路径和文件名;
  3. 可选择是否勾选 Export with Debug,初次导出或测试阶段建议勾选,便于查看运行日志,正式发布版本可以取消勾选;
  4. 点击确认后,Godot 会开始打包并生成 .exe 文件;
  5. 关闭对话菜单,打开你设置的导出路径,你会看到生成的 MyTankGame.exe 文件。
  6. 打开导出目录,双击生成的 MyTankGame.exe 文件即可运行游戏。如果导出时启用了 Debug 模式,游戏运行时会额外打开一个终端窗口,用于显示调试信息。

6.5 打包和发布建议

你可以将这个可执行文件进行压缩成一个zip文件,方便分享到其他电脑,其它用户解压后双击exe文件即可开始游戏,无需安装 Godot。你还可以在发布时为你的游戏命名加上版本号,如 MyTankGame_v1.0.exe,方便后续更新。另外你可以准备几张游戏截图,便于宣传。压缩包中可以附带一个 README.txt,简单说明游戏玩法、操作方式和作者信息。

一旦你学会将游戏打包发布到Windows平台,你还可以尝试:

  • 上传到 itch.io、Steam 等平台供其它玩家下载;
  • 通过社交媒体分享给朋友试玩,收集反馈;
  • 继续尝试发布到其他平台,如 HTML5(网页格式),这样通过浏览器就可以玩你的游戏。也可以发布到Android或 Mac,不过需要对应的开发账户和额外的配置。

游戏发布是从“开发者”走向“分享者”的关键一步。通过 Godot 的导出功能,我们可以非常方便地将游戏发布为 Windows 可执行文件,并交付给玩家使用。即使是初学者,只需简单配置,就能轻松实现发布,迈出成为游戏开发者的第一步!

本章小结

本章是整个游戏开发流程的最后一步,我们围绕“让游戏更完整、更可发布”这一目标,完成了多个方面的重要优化与扩展。

首先,在视觉表现方面,我们介绍了 2D 场景中的光线与阴影系统,使用 CanvasModulate 创建整体氛围色调,配合点光源和光线蒙版增强光影效果;我们还简要了解了 WorldEnvironment、HDR 和 Glow 的基础知识,为游戏画面增添专业质感。

随后,我们优化了主菜单系统,设计了交互流畅、音效反馈完整的界面,并通过GameManager实现菜单与游戏关卡之间的切换逻辑。同时,我们构建了SoundManager,让背景音乐与交互音效可以跨场景播放与统一管理。

在关卡管理方面,我们设计并实现了LevelManager,它控制每一关的提示、胜负判定、暂停与继续等逻辑,使游戏流程更流畅自然。配合新的 UI 动画和信息面板,玩家体验更具节奏感和沉浸感。我们还通过简单判断实现了“道路加速”机制,使关卡地图中的地形设计更具意义。

最后,我们带领读者完成了游戏的发布流程,从配置导出模板、设置图标、选择资源打包方式,到生成 .exe 文件,让每一位初学者都能将自己的作品真正发布出来,与他人分享。

通过这一章的学习,你已经完成了从“能运行的作品”到“可交付的游戏”的重要跨越。接下来,你可以继续扩展玩法、优化体验,或尝试将项目上传到 itch.io、Steam 等平台,迈出独立开发者的第一步!