三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Godot 4 3D游戏开发入门:从零构建滚球收集游戏原型

Godot 4 3D游戏开发入门:从零构建滚球收集游戏原型

1. 项目概述:为什么是Godot,为什么是现在?

如果你和我一样,是个在游戏行业摸爬滚打多年的“老家伙”,看着Unity的收费风波、Unreal的庞大身躯,心里总会琢磨:有没有一个轻量、自由、又能打的引擎,能让我们这些独立开发者或小团队,把精力真正花在创意上,而不是在复杂的管线、高昂的成本和臃肿的编辑器里挣扎?Godot,就是那个让我眼前一亮的答案。它不是突然冒出来的新秀,而是经过多年社区打磨,在4.0版本后真正在3D领域站稳脚跟的开源利器。这次,我们不谈空洞的概念,就从一个最具体的目标开始:用Godot,从零开始,完整地“搓”出一个能跑起来的3D游戏。这个过程,我会把我踩过的坑、验证过的技巧,以及那些官方文档里不会明说的“潜规则”,毫无保留地摊开来讲。无论你是刚接触游戏开发的新手,还是从其他引擎转过来的老鸟,这篇笔记的目的就是让你能跟着做,做出东西,并理解每一步背后的“所以然”。

2. 核心思路与项目选型:定义一个“最小可行游戏”

在动手写第一行代码之前,最重要的一步是明确我们要做什么。对于第一个3D游戏,目标不是复刻《塞尔达传说》,而是构建一个“最小可行产品”(MVP),它能验证核心玩法循环,并涵盖3D游戏开发的基础模块。我选择了“3D滚球收集”这个经典原型。它麻雀虽小,五脏俱全:需要处理3D物理运动、摄像机跟随、用户输入、碰撞检测、UI更新和简单的游戏状态管理。选择Godot 4作为引擎,不仅因为其开源免费(这对个人和独立开发者至关重要),更因为其独特的场景树(Scene Tree)节点架构,让游戏对象的组织逻辑非常清晰,学习曲线相对平缓。与Unity的GameObject/Component模式或Unreal的Actor/Component模式相比,Godot的“一切皆节点,节点可嵌套”的理念,在构建复杂游戏对象时,往往更直观。

2.1 为什么不是Unity或Unreal?

对于初学者或小型项目,Unity曾经是毋庸置疑的起点,但其新的运行时费用政策增加了很多不确定性,尤其是对成功的个人开发者或小团队。Unreal Engine功能强大,画面顶尖,但其庞大的体量、复杂的C++/蓝图双线学习路径,以及5%的收入分成(虽然门槛不低),对于想快速验证想法的初学者来说,显得过于沉重。Godot则完全没有这些顾虑。一个不到100MB的安装包,包含了编辑器、脚本语言(GDScript,类似Python,极易上手)、调试工具和导出模板。你可以用它做任何你想做的项目,无论成功与否,都没有财务或法律上的后顾之忧。这就是自由的味道。

2.2 工具链准备:安装与初识编辑器

首先,去Godot官网下载最新的稳定版(目前是4.2.x)。建议下载标准版(Standard build),它包含了所有功能。安装过程就是解压一个文件夹,绿色便携,甚至可以放在U盘里随身携带。

第一次打开Godot,你会看到项目管理器。点击“新建项目”,给它起个名字,比如“MyFirst3DBall”。选择渲染器:如果你的显卡比较新(支持Vulkan),强烈建议选择“Forward+”,它能提供更好的光影效果和性能;如果遇到兼容性问题,再回退到“兼容性”模式。项目路径不要有中文或特殊字符,这是一个好习惯。

创建后进入主编辑器。界面可能有点陌生,但别慌,核心就这几块:

  • 场景面板(左上):以树形结构展示当前场景的所有节点。这是我们构建游戏世界的地方。
  • 视口(中间):3D/2D视图,预览和编辑场景。
  • 文件系统面板(左下):项目资源管理器,所有场景、脚本、图片、模型都在这。
  • 检查器面板(右):显示当前选中节点的所有属性和参数,修改它们就能改变节点行为。
  • 底部面板:输出、调试、动画编辑器等会在这里打开。

花几分钟拖动各个面板,熟悉一下布局。接下来,我们开始真正的建造。

3. 构建游戏世界:场景、节点与第一个3D对象

Godot的核心哲学是“场景(Scene)驱动”。一个场景就是一棵节点树,它可以是一个角色、一把武器、一个关卡,甚至整个游戏界面。我们将从创建一个简单的游戏关卡场景开始。

3.1 创建基础场景与地面

  1. 在“场景”面板中,你看到一个叫“Scene”的根节点。默认是Node3D类型,这是所有3D节点的基类。我们先把它重命名为“Level”(选中节点按F2)。
  2. 我们需要一个地面。右键点击“Level”节点,选择“添加子节点”。在搜索框中输入“MeshInstance3D”,添加它。这是一个用于显示3D网格的节点。
  3. 选中新建的MeshInstance3D节点,看右侧的“检查器”面板。找到“Mesh”属性,点击旁边的下拉箭头,选择“新建BoxMesh”。一个白色的立方体出现在视口中。
  4. 这个立方体现在是我们的“原型地面”。我们需要把它压扁、放大。在检查器中,找到“Transform”下的“Scale”(缩放)。将Y轴(第二个值)设为0.2,将X和Z轴(第一和第三个值)设为10。现在你得到了一个扁平的长方形地面。
  5. 为了让物理生效,我们必须给地面添加一个碰撞体。选中MeshInstance3D节点,点击检查器顶部的“添加子节点”按钮(或右键节点),选择“CollisionShape3D”。Godot会自动为其创建一个CollisionShape3D子节点。
  6. 选中这个CollisionShape3D节点,在检查器中,点击“Shape”属性旁的下拉箭头,选择“新建BoxShape3D”。你会看到一个绿色的线框包裹住地面,这代表了物理碰撞的体积。关键点:碰撞形状(Shape)的尺寸默认是(1,1,1),它基于父节点MeshInstance3D的缩放。因为我们把地面网格缩放了(10, 0.2, 10),所以碰撞盒也会自动同步这个缩放比例,完美匹配可视网格。这是Godot一个非常方便的特性。

注意:在3D游戏中,确保碰撞体与可视网格基本匹配至关重要,否则会出现“视觉上没碰到,但游戏判定碰撞了”或者“穿模”的诡异情况。对于简单形状,Godot的自动同步很好用;对于复杂模型,可能需要手动调整碰撞形状或使用多个简单形状组合。

3.2 创建我们的主角:可控制的球体

主角是一个受物理控制的球体。

  1. 回到场景根节点“Level”,右键添加一个新的子节点,类型为“CharacterBody3D”。这是Godot 4中用于处理角色移动的推荐节点,它内部集成了碰撞检测和物理响应,比直接用RigidBody3D(刚体)更容易控制。将其重命名为“PlayerBall”。
  2. 选中“PlayerBall”节点,为其添加一个MeshInstance3D子节点。将其Mesh属性设为“新建SphereMesh”。你可以调整球体的半径(在Mesh资源属性里),比如设为0.5。
  3. 同样,为“PlayerBall”节点添加一个CollisionShape3D子节点。将其Shape属性设为“新建SphereShape3D”。调整SphereShape3D的半径,使其与可视球体匹配(例如0.5)。
  4. 现在,我们需要让球体动起来。这需要脚本。选中“PlayerBall”节点,点击检查器上方类似纸卷的图标“附加脚本”。保持默认设置(语言:GDScript,模板:默认),点击“创建”。这会创建一个关联到该节点的脚本文件。

4. 编写核心逻辑:移动、摄像与交互

双击新创建的脚本文件,Godot的内置脚本编辑器会打开。我们将在这里编写控制逻辑。

4.1 玩家移动脚本解析

我们先编写一个基础的、使用物理力的移动脚本。请替换脚本模板中的内容为以下代码:

extends CharacterBody3D # 定义可调节的参数 @export var speed = 10.0 @export var jump_velocity = 8.0 @export var mouse_sensitivity = 0.002 # 获取摄像机节点 @onready var camera_pivot = $CameraPivot @onready var camera = $CameraPivot/Camera3D func _ready(): # 游戏开始时,锁定鼠标到窗口,用于控制摄像机视角 Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) func _unhandled_input(event): # 处理鼠标移动事件,控制摄像机旋转 if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: # 左右移动鼠标控制水平旋转(绕Y轴) rotate_y(-event.relative.x * mouse_sensitivity) # 上下移动鼠标控制摄像机俯仰(绕X轴),并限制角度避免翻转 camera_pivot.rotate_x(-event.relative.y * mouse_sensitivity) camera_pivot.rotation.x = clamp(camera_pivot.rotation.x, deg_to_rad(-60), deg_to_rad(60)) # 按ESC键释放鼠标 if event.is_action_pressed("ui_cancel"): if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) else: Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) func _physics_process(delta): # 每帧物理步进时调用,处理移动和物理逻辑 # 初始化速度向量 var direction = Vector3.ZERO # 获取基于玩家当前朝向的输入方向 if Input.is_action_pressed("move_forward"): direction -= transform.basis.z if Input.is_action_pressed("move_backward"): direction += transform.basis.z if Input.is_action_pressed("move_left"): direction -= transform.basis.x if Input.is_action_pressed("move_right"): direction += transform.basis.x # 归一化方向向量,防止斜向移动更快 if direction.length() > 0: direction = direction.normalized() # 应用水平速度 velocity.x = direction.x * speed velocity.z = direction.z * speed # 处理跳跃(仅在地面时) if is_on_floor() and Input.is_action_just_pressed("jump"): velocity.y = jump_velocity # 应用重力(如果不在跳跃状态) if not is_on_floor(): velocity.y -= 9.8 * delta # 简单模拟重力 # 调用父方法,执行移动和碰撞检测 move_and_slide()

代码要点解析:

  1. @export关键字:这是Godot 4的一个强大功能。它将变量暴露在编辑器的检查器中,你可以在不修改代码的情况下,直接在编辑器里调整速度、跳跃力等参数,实现快速迭代。
  2. _ready():场景加载完成后调用一次。这里我们锁定鼠标,实现常见的“鼠标控制视角”操作。
  3. _unhandled_input(event):处理未消耗的输入事件。我们用它来捕获鼠标移动,并计算摄像机的旋转。clamp函数用于限制俯仰角度,防止摄像机翻转过头。
  4. _physics_process(delta):在固定的物理时间步长(默认每秒60次)中调用。这是处理移动、力和碰撞的最佳位置。
  5. transform.basis.ztransform.basis.x:这代表了该节点局部坐标系的前方(-Z)和右方(+X)。通过将输入映射到这些方向上,我们实现了“按W向前走”是基于玩家当前朝向的,而不是固定的世界坐标。
  6. move_and_slide()CharacterBody3D的核心方法。它根据velocity向量移动角色,并自动处理与场景中其他CollisionShape3D的碰撞和滑动。调用后,is_on_floor()等方法的状态会被更新。

4.2 配置输入映射

脚本里用到了move_forwardjump等动作(Action),我们需要在项目设置中定义它们。

  1. 点击编辑器顶部菜单:项目 -> 项目设置。
  2. 切换到“输入映射”标签页。
  3. 在“动作”输入框,输入“move_forward”,点击“添加”。然后点击其旁边的“+”号,添加一个键盘事件,选择“W”键。同样地,为“move_backward”添加“S”键,“move_left”添加“A”键,“move_right”添加“D”键。
  4. 添加“jump”动作,并绑定“空格键”。
  5. 添加“ui_cancel”动作(Godot已有默认,但检查一下),通常绑定“Esc”键。

4.3 设置摄像机

我们的脚本引用了CameraPivotCamera3D节点,现在来创建它们。

  1. 在场景中,选中“PlayerBall”节点。
  2. 右键添加一个子节点,类型为“Node3D”,重命名为“CameraPivot”。这个节点将负责摄像机的俯仰旋转。
  3. 选中“CameraPivot”节点,右键添加一个子节点,类型为“Camera3D”,重命名为“Camera3D”。
  4. 选中“Camera3D”节点,在检查器中,调整它的位置。将“Transform”下的“Position”的Y值设为1.0(让摄像机在球体上方1米处),Z值设为-3.0(让摄像机在球体后方3米处)。这样就是一个经典的第三人称跟随视角。
  5. 重要:为了让摄像机平滑跟随,而不是僵硬地绑在球上,我们可以使用一个简单的插值。修改脚本,在_physics_process函数的最后,move_and_slide()调用之后,添加以下代码:
# 平滑摄像机跟随(可选,让摄像机更柔和地跟随球体) var target_position = global_transform.origin target_position.y += 1.0 # 摄像机高度偏移 camera_pivot.global_transform.origin = camera_pivot.global_transform.origin.lerp(target_position, delta * 10.0)

lerp方法是线性插值,让摄像机的位置每一帧向目标位置(球体位置+偏移)移动一小段距离,从而实现平滑的跟随效果,避免剧烈抖动。

5. 丰富游戏性:收集物、UI与游戏状态

现在我们的球可以滚了,但游戏还需要目标和反馈。

5.1 创建可收集的硬币

  1. 在场景根节点“Level”下,创建一个新的RigidBody3D节点,重命名为“Coin”。
  2. 为它添加MeshInstance3D子节点,使用“新建CylinderMesh”作为网格。在网格属性中,将“顶部半径”和“底部半径”设为0.5,“高度”设为0.2,让它看起来像个硬币。可以再添加一个OmniLight3D(点光源)作为子节点,赋予一点发光效果。
  3. 为“Coin”添加CollisionShape3D,形状使用“新建CylinderShape3D”,调整尺寸匹配网格。
  4. 我们希望硬币被碰到后就消失并计分。为“Coin”节点附加一个新脚本。
extends RigidBody3D # 定义一个信号,当硬币被收集时发出 signal collected func _on_body_entered(body): # 当有物理体进入碰撞区域时调用 # 检查进入的是否是玩家球体 if body.is_in_group("player"): # 发出收集信号 emit_signal("collected") # 播放一个简单的消失动画(例如缩放至0) var tween = create_tween() tween.tween_property(self, "scale", Vector3.ZERO, 0.2) tween.tween_callback(queue_free) # 动画结束后从场景中删除自己
  1. 在场景中,选中“Coin”的根节点,在检查器的“节点”标签页,点击“组”旁边的编辑按钮,为硬币添加一个组,比如“coin”。方便以后用代码查找所有硬币。
  2. 我们需要让玩家球体属于“player”组。选中“PlayerBall”节点,同样在“节点”标签页,为其添加“player”组。
  3. 最后,我们需要在“PlayerBall”的脚本中连接硬币的信号并处理得分。在_ready()函数末尾添加:
func _ready(): Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) # 查找场景中所有的硬币,并连接它们的收集信号 for coin in get_tree().get_nodes_in_group("coin"): coin.collected.connect(_on_coin_collected) func _on_coin_collected(): # 增加分数,这里我们先打印到控制台,后面会连接到UI print("Coin collected!") # 在实际项目中,这里会更新一个全局的分数变量,并通知UI更新

5.2 创建用户界面(UI)

Godot的UI系统基于控件(Control)节点。

  1. 首先,我们需要一个独立的UI场景。在文件系统面板右键,选择“新建场景”。创建一个CanvasLayer节点作为根。CanvasLayer可以确保UI渲染在所有3D内容之上。
  2. 重命名根节点为“UILayer”。然后为其添加一个Control子节点,命名为“MainUI”。
  3. 选中“MainUI”,在检查器中,找到“布局”下拉菜单,选择“全矩形”。这样UI就会铺满整个屏幕。
  4. 现在添加显示元素。右键点击“MainUI”,添加一个Label子节点,重命名为“ScoreLabel”。在检查器中,调整其“文本”为“分数:0”。你可以拖动它在视口中的位置,或者使用“锚点”和“边距”进行精确定位(推荐)。例如,将锚点的水平预设设为“左上”,垂直预设也设为“左上”,然后调整上边距和左边距为20像素。
  5. 再添加一个Button节点,重命名为“RestartButton”。设置其文本为“重新开始”,并调整位置(比如锚定在中心)。暂时先隐藏它(取消勾选检查器顶部的“可见”属性),当游戏结束时才显示。
  6. 为UI场景保存,命名为“UI.tscn”。
  7. 回到我们的主场景“Level”。在场景根节点下添加一个子节点,类型为“Node”(作为UI的挂载点),重命名为“UI”。然后,点击检查器中的“场景”属性旁边的链接图标,选择我们刚保存的“UI.tscn”场景。这样就将UI场景实例化到了主场景中。
  8. 我们需要在“PlayerBall”脚本中引用UI并更新分数。首先,在脚本顶部声明一个变量:@onready var ui = $”../UI/UILayer/MainUI”(路径根据你的节点结构调整)。然后修改_on_coin_collected函数:
var score = 0 @onready var score_label = ui.get_node(“ScoreLabel”) # 获取分数标签节点 func _on_coin_collected(): score += 1 score_label.text = “分数:%d” % score # 检查是否收集完所有硬币 if score >= 5: # 假设有5个硬币 _on_game_won() func _on_game_won(): print(“You Win!”) # 显示重新开始按钮 var restart_button = ui.get_node(“RestartButton”) restart_button.visible = true # 可以暂停游戏物理 get_tree().paused = true
  1. 最后,为“RestartButton”添加按下信号的处理。在UI场景中,选中“RestartButton”,在检查器的“节点”标签页,切换到“信号”子标签,找到“pressed()”信号,双击连接,选择“MainUI”节点,创建一个新函数。在该函数中写入:get_tree().reload_current_scene()。这会重新加载当前场景,实现重启。

6. 打磨与发布:优化、调试与导出

6.1 环境与光照

目前我们的场景只有默认的灰色环境光。为了让游戏看起来更舒服,我们需要设置世界环境。

  1. 在主场景中,选中根节点“Level”。
  2. 在检查器中,找到“世界环境”属性(World Environment),点击下拉箭头选择“新建Environment”。
  3. 点击新建的Environment资源进行编辑。你可以调整“背景”模式为“纯色”或“天空”,并设置颜色。在“光照”部分,可以启用“环境光”,并设置其颜色和能量,这相当于一个全局的基础光照,能照亮所有物体的背光面。
  4. 为了更好的效果,可以添加一个DirectionalLight3D(平行光)节点到场景中,模拟太阳光。调整它的旋转角度,照亮场景。

6.2 调试技巧与常见问题

  • 问题:球体移动“滑冰感”太强,停不下来。

    • 原因CharacterBody3Dmove_and_slide()默认会保留一部分速度。我们在_physics_process中直接设置了velocity.xvelocity.z,但没有处理减速。
    • 解决:在应用输入速度前,先给水平速度一个衰减。可以在计算direction后添加:
      # 水平速度衰减(模拟摩擦力) velocity.x = lerp(velocity.x, 0.0, delta * 5.0) velocity.z = lerp(velocity.z, 0.0, delta * 5.0) # 然后再 velocity.x = direction.x * speed ...
      lerp在这里用于平滑地将速度向0衰减。
  • 问题:摄像机旋转时,球体也跟着乱转。

    • 原因:我们的摄像机旋转代码是直接作用在PlayerBall节点(rotate_y)和CameraPivot节点上的。rotate_y会旋转整个球体节点,导致其前进方向改变。
    • 解决:这是设计选择。对于“坦克式”控制(移动方向独立于视角),应该将水平旋转应用在一个独立的“YawPivot”节点上,而球体模型作为其子节点。对于“第一人称/第三人称”控制(移动方向跟随视角),当前做法是合理的。根据你的游戏类型调整节点结构。
  • 问题:导出项目时报错或找不到模板。

    • 解决:首次导出前,需要下载导出模板。点击编辑器顶部“项目” -> “导出”。在“导出预设”中,添加一个预设(如“Windows Desktop”)。在右侧的“功能”中,选择“Export With Debug”(调试导出)或“Export”(发布导出)。底部会提示“模板缺失”,点击“下载”按钮,Godot会自动下载对应平台的导出模板。确保下载的模板版本与你的Godot引擎版本完全一致

6.3 导出为可执行文件

  1. 点击编辑器右上角的“导出项目”按钮(或通过“项目”->“导出”菜单)。
  2. 确保你已经为你的目标平台(如Windows)添加了导出预设并下载了模板。
  3. 在预设中,你可以配置应用名称、图标、文件包含规则等。
  4. 点击“导出项目”按钮,选择一个输出路径和文件名(例如MyFirst3DBall.exe)。
  5. Godot会打包所有资源并生成可执行文件。运行它,你的第一个Godot 3D游戏就诞生了!

7. 进阶思考与项目扩展

完成这个基础原型后,你已经走过了Godot 3D开发的核心路径:场景搭建、节点组织、物理交互、输入处理、脚本逻辑、UI连接和项目导出。但这仅仅是开始。基于这个框架,你可以尝试以下扩展,让游戏变得更完整:

  • 添加敌人与生命值:创建另一个CharacterBody3D作为敌人,为其编写简单的AI(如朝玩家移动)。为玩家添加生命值变量,在碰撞到敌人时减少,并在UI上显示。
  • 设计多个关卡:将当前的“Level”场景保存为“Level1.tscn”。然后新建场景“Level2.tscn”,设计不同的地形和障碍。创建一个“GameManager”单例(Autoload)来管理关卡切换和全局状态(如总分数)。
  • 加入音效:使用AudioStreamPlayer3D节点为硬币收集、跳跃等动作添加空间音效,使用AudioStreamPlayer节点添加背景音乐。
  • 优化性能:对于大量重复的物体(如硬币),考虑使用MultiMeshInstance3D进行合批渲染。使用Godot的性能分析器(调试器 -> 分析器)查看帧时间和Draw Call。
  • 探索更高级的特性:尝试Godot的着色器语言(GLSL或Godot自带的着色器语言)来制作特效,使用动画播放器(AnimationPlayer)为角色添加跑动、跳跃动画,或者用导航网格(NavigationRegion3D)为敌人实现更复杂的路径寻找。

Godot的魅力在于,它用一个相对简洁的体系,覆盖了从原型到完整产品的大多数需求。它的文档和活跃的社区是宝贵的学习资源。记住,学习游戏开发最好的方式就是动手去做,遇到问题,拆解问题,然后解决它。从这个滚动的球开始,去构建你想象中的世界吧。

← 返回列表