2. 项目搭建与场景配置
项目搭建与场景配置
2.1 创建Godot 4项目
首先,我们需要创建一个新的Godot 4项目。如果你还没有安装Godot 4,请先从Godot官网下载安装。
Godot版本要求
本教程使用 Godot 4.x 版本。Godot 4相比Godot 3有很多改进,特别是3D物理引擎和渲染系统都有了很大提升,非常适合做桌球这种需要精确物理模拟的游戏。
创建项目步骤
- 打开Godot 4编辑器
- 点击新建项目(New Project)
- 项目名称填入
Billiards(桌球) - 选择一个你喜欢的存放路径
- 渲染器选择 Forward+(默认选项,适合大多数3D游戏)
- 点击创建并编辑(Create & Edit)
项目设置
创建好项目后,我们需要做一些基础设置。点击菜单栏 项目 → 项目设置(Project → Project Settings):
| 设置项 | 值 | 说明 |
|---|---|---|
| 应用程序 → 名称 | 桌球 / Billiards | 游戏显示名称 |
| 应用程序 → 窗口 → 宽度 | 1280 | 窗口宽度 |
| 应用程序 → 窗口 → 高度 | 720 | 窗口高度 |
| 应用程序 → 窗口 → 模式 | 窗口化 | 可以全屏 |
| 通用 → 物理引擎 → 3D → 默认重力Y | 0 | 桌球不需要重力 |
关闭重力!
桌球是在水平桌面上进行的,不需要像平台游戏那样的重力。把默认重力设为0非常重要,否则球会"往下掉"!
物理引擎设置
桌球需要精确的物理模拟,所以我们要调整物理引擎的参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 物理 → 3D → Solver → Solver Iterations | 16 | 碰撞求解迭代次数,越高越精确 |
| 物理 → 3D → Solver → Contact Recycle Radius | 0.01 | 接触回收半径 |
| 物理 → 3D → Sleep → Enable Sleeping | 开启 | 球停下后休眠,节省性能 |
| 物理 → 3D → Sleep → Linear Sleep Threshold | 0.05 | 速度低于此值时进入休眠 |
什么是Solver Iterations?
你可以把它理解为物理引擎的"计算精度"——就像你做数学题时,检查的次数越多,结果越准确。桌球需要精确的碰撞,所以设高一些(默认是8,我们设为16)。
2.2 场景结构规划
在开始搭建场景之前,先规划好整个游戏的场景结构。良好的场景结构能让开发过程事半功倍。
场景树结构
BilliardsGame (主场景 - Node3D)
├── Camera (摄像机)
├── Environment (环境光)
├── TableLight (台球灯)
├── Table (球桌)
│ ├── TableSurface (桌面 - CSGBox3D)
│ ├── TableFrame (边框 - 多个CSGBox3D)
│ ├── Pocket1~6 (球袋 - Area3D)
│ └── RailCushion (缓冲垫)
├── Balls (所有球)
│ ├── CueBall (母球 - RigidBody3D)
│ ├── Ball_1 (1号球 - RigidBody3D)
│ ├── Ball_2 (2号球 - RigidBody3D)
│ ├── ...
│ └── Ball_15 (15号球 - RigidBody3D)
├── UIManager (UI层)
│ ├── AimLine (瞄准线)
│ ├── PowerBar (力度条)
│ ├── ScoreBoard (计分板)
│ └── TurnIndicator (回合指示)
└── GameManager (游戏管理器)场景树就像家族树
场景树(Scene Tree)就是游戏中所有对象的"家族树"。每个节点都是一个"家庭成员",它们之间有父子关系。父节点移动时,子节点跟着移动——就像桌子移动时,桌子上的球也跟着移动一样。
2.3 摄像机设置
摄像机是玩家观察游戏的"眼睛"。对于2.5D桌球游戏,我们需要一个从上方略带倾斜的视角。
创建摄像机
- 在场景树中右键点击根节点
BilliardsGame - 选择 添加子节点(Add Child Node)
- 搜索并添加 Camera3D 节点
- 将其重命名为
Camera
摄像机参数设置
选中摄像机节点,在右侧的检查器(Inspector)面板中设置以下参数:
| 属性 | 值 | 说明 |
|---|---|---|
| Transform → Position | (0, 12, 6) | 摄像机位置 |
| Transform → Rotation | (-63, 0, 0) | 摄像机旋转角度 |
| Projection | 透视(Perspective) | 透视投影 |
| FOV | 45 | 视场角 |
| Near | 0.05 | 近裁剪面 |
| Far | 100 | 远裁剪面 |
透视 vs 正交
- 透视(Perspective):近大远小,有立体感,更真实
- 正交(Orthographic):没有近大远小,像看地图
桌球游戏推荐使用透视模式,因为能看到球的立体感。但如果你想要更"平面"的感觉,也可以用正交模式。
摄像机视角对比
| 视角类型 | 位置示例 | 旋转示例 | 效果 |
|---|---|---|---|
| 完全俯视 | (0, 15, 0) | (-90, 0, 0) | 像看地图,没有立体感 |
| 略微倾斜(推荐) | (0, 12, 6) | (-63, 0, 0) | 有立体感,操作直观 |
| 较低角度 | (0, 8, 10) | (-38, 0, 0) | 立体感很强,但操作不太方便 |
| 玩家视角 | (0, 3, 12) | (-15, 0, 0) | 像站在桌边看,太难瞄准 |
摄像机控制器代码
为了方便调试和后续可能的功能扩展(比如视角切换),我们来写一个简单的摄像机控制器。
using Godot;
/// <summary>
/// 摄像机控制器 - 管理2.5D桌球视角
/// </summary>
public partial class CameraController : Camera3D
{
/// <summary>默认摄像机位置</summary>
[Export] public Vector3 DefaultPosition { get; set; } = new Vector3(0, 12, 6);
/// <summary>默认摄像机旋转角度</summary>
[Export] public Vector3 DefaultRotation { get; set; } = new Vector3(-63, 0, 0);
/// <summary>平滑过渡的速度</summary>
[Export] public float SmoothSpeed { get; set; } = 5.0f;
/// <summary>目标位置(用于平滑过渡)</summary>
private Vector3 _targetPosition;
/// <summary>目标旋转(用于平滑过渡)</summary>
private Vector3 _targetRotation;
public override void _Ready()
{
// 初始化摄像机到默认位置
Position = DefaultPosition;
RotationDegrees = DefaultRotation;
_targetPosition = DefaultPosition;
_targetRotation = DefaultRotation;
}
public override void _Process(double delta)
{
// 平滑过渡到目标位置和旋转
Position = Position.Lerp(_targetPosition, (float)(SmoothSpeed * delta));
RotationDegrees = RotationDegrees.Lerp(_targetRotation, (float)(SmoothSpeed * delta));
}
/// <summary>
/// 切换到默认俯视角
/// </summary>
public void ResetToDefault()
{
_targetPosition = DefaultPosition;
_targetRotation = DefaultRotation;
}
/// <summary>
/// 移动到指定位置
/// </summary>
/// <param name="position">目标位置</param>
/// <param name="rotation">目标旋转</param>
public void MoveTo(Vector3 position, Vector3 rotation)
{
_targetPosition = position;
_targetRotation = rotation;
}
/// <summary>
/// 聚焦到球桌上某个点
/// </summary>
/// <param name="worldPosition">要聚焦的世界坐标</param>
public void FocusOnPoint(Vector3 worldPosition)
{
_targetPosition = new Vector3(
worldPosition.X,
DefaultPosition.Y,
worldPosition.Z + DefaultPosition.Z - worldPosition.Z
);
}
}## 摄像机控制器 - 管理2.5D桌球视角
extends Camera3D
## 默认摄像机位置
@export var default_position: Vector3 = Vector3(0, 12, 6)
## 默认摄像机旋转角度
@export var default_rotation: Vector3 = Vector3(-63, 0, 0)
## 平滑过渡的速度
@export var smooth_speed: float = 5.0
## 目标位置(用于平滑过渡)
var _target_position: Vector3
## 目标旋转(用于平滑过渡)
var _target_rotation: Vector3
func _ready() -> void:
# 初始化摄像机到默认位置
position = default_position
rotation_degrees = default_rotation
_target_position = default_position
_target_rotation = default_rotation
func _process(delta: float) -> void:
# 平滑过渡到目标位置和旋转
position = position.lerp(_target_position, smooth_speed * delta)
rotation_degrees = rotation_degrees.lerp(_target_rotation, smooth_speed * delta)
## 切换到默认俯视角
func reset_to_default() -> void:
_target_position = default_position
_target_rotation = default_rotation
## 移动到指定位置
func move_to(pos: Vector3, rot: Vector3) -> void:
_target_position = pos
_target_rotation = rot
## 聚焦到球桌上某个点
func focus_on_point(world_position: Vector3) -> void:
_target_position = Vector3(
world_position.x,
default_position.y,
world_position.z + default_position.z - world_position.z
)2.4 球桌场景搭建
球桌是桌球游戏的核心场景元素。一个标准的球桌由桌面、边框、缓冲垫和6个球袋组成。
球桌尺寸
真实尺寸参考
一张标准的美式台球桌,内部打球区域大约是 2.54米 x 1.27米(比例约2:1)。在Godot中,我们用米作为单位,所以桌面大约是 2.54 x 1.27。不过为了方便计算,我们可以稍微调整一下。
| 部件 | 尺寸(Godot单位) | 说明 |
|---|---|---|
| 桌面(内部) | 2.54 x 0.05 x 1.27 | 绿色台呢 |
| 边框宽度 | 0.15 | 四周的木边框 |
| 桌面高度(Y轴) | 0.8 | 桌面离地面的高度 |
| 球袋半径 | 0.06 | 球袋开口大小 |
| 球的半径 | 0.028 | 标准桌球大小 |
创建桌面
- 在场景树中添加 CSGBox3D 节点,命名为
TableSurface - 设置尺寸为
2.54 x 0.05 x 1.27 - 设置位置为
(0, 0.8, 0) - 创建一个绿色材质(
StandardMaterial3D):- Albedo Color:
#0a6e2e(深绿色,模拟台呢) - Roughness:
0.8(粗糙度高,模拟布料质感)
- Albedo Color:
创建边框
球桌有4条边框,分别在上、下、左、右四个方向。每条边框用一个 CSGBox3D 来表示。
| 边框 | 位置 | 尺寸 | 说明 |
|---|---|---|---|
| 上边框 | (0, 0.8, -0.71) | 2.84 x 0.1 x 0.15 | Z轴负方向 |
| 下边框 | (0, 0.8, 0.71) | 2.84 x 0.1 x 0.15 | Z轴正方向 |
| 左边框 | (-1.345, 0.8, 0) | 0.15 x 0.1 x 1.57 | X轴负方向 |
| 右边框 | (1.345, 0.8, 0) | 0.15 x 0.1 x 1.57 | X轴正方向 |
边框的作用
边框有两个作用:
- 视觉上:给球桌一个漂亮的边框
- 物理上:作为碰撞体,让球撞到边框后反弹
我们后面会给边框添加碰撞形状来实现物理反弹。
创建球袋
标准球桌有6个球袋:4个角袋 + 2个中袋。
| 球袋 | 位置 | 说明 |
|---|---|---|
| 左上角袋 | (-1.27, 0.8, -0.635) | 左前角 |
| 右上角袋 | (1.27, 0.8, -0.635) | 右前角 |
| 左下角袋 | (-1.27, 0.8, 0.635) | 左后角 |
| 右下角袋 | (1.27, 0.8, 0.635) | 右后角 |
| 上中袋 | (0, 0.8, -0.635) | 上方中间 |
| 下中袋 | (0, 0.8, 0.635) | 下方中间 |
球袋用 Area3D 节点来创建(后面章节详细讲解),并添加一个球形碰撞形状作为检测区域。
2.5 灯光设置
好的灯光能让桌球游戏看起来非常真实。在现实中,球桌上方会有一盏专门的台球灯。
环境光
环境光是"无处不在"的光,让整个场景不会一片漆黑。
- 添加 WorldEnvironment 节点
- 在其
Environment属性中:- Ambient Light → Color:
#404040(偏暗的环境光) - Ambient Light → Energy:
0.3
- Ambient Light → Color:
台球灯(主光源)
台球灯是一盏从上方照下来的灯,模拟球桌上方的专业照明。
- 添加 DirectionalLight3D 节点,命名为
TableLight - 设置位置为
(0, 5, 0)(在球桌正上方) - 设置旋转为
(-90, 0, 0)(垂直向下照) - 参数设置:
| 参数 | 值 | 说明 |
|---|---|---|
| Light Color | #fff5e0 | 暖白色,模拟台球灯 |
| Light Energy | 3.0 | 光照强度 |
| Shadow → Enabled | 开启 | 开启阴影 |
| Shadow → Opacity | 0.6 | 阴影透明度 |
为什么用暖白色?
真实的台球灯通常是暖色调的(偏黄),因为暖光能让球桌看起来更温馨、更专业。冷白色的光会让人感觉像在办公室,而不是在打台球。
补光(可选)
如果场景某些角落太暗,可以添加一盏微弱的补光:
- 添加 DirectionalLight3D 节点,命名为
FillLight - 设置位置为
(3, 4, 3) - 设置旋转为
(-60, -30, 0) - 参数:
- Light Energy:
0.3(很弱) - Shadow: 关闭
- Light Energy:
2.6 材质配置
材质决定了物体的外观——颜色、质感、反光程度等。桌球游戏需要以下几种材质:
台呢材质(桌面绿色布料)
| 属性 | 值 | 说明 |
|---|---|---|
| Albedo Color | #0a6e2e | 深绿色 |
| Roughness | 0.85 | 高粗糙度,模拟布料 |
| Metallic | 0.0 | 无金属感 |
木框材质(球桌边框)
| 属性 | 值 | 说明 |
|---|---|---|
| Albedo Color | #5c3317 | 深棕色 |
| Roughness | 0.4 | 中等粗糙度,模拟木头上漆 |
| Metallic | 0.1 | 微弱的金属感 |
球袋材质
| 属性 | 值 | 说明 |
|---|---|---|
| Albedo Color | #1a1a1a | 纯黑色 |
| Roughness | 0.9 | 高粗糙度 |
母球材质
| 属性 | 值 | 说明 |
|---|---|---|
| Albedo Color | #f5f5f0 | 微黄白色 |
| Roughness | 0.15 | 低粗糙度,有光泽 |
| Metallic | 0.0 | 无金属感 |
| Clearcoat | 1.0 | 清漆效果,模拟光滑表面 |
材质的关键参数
- Albedo:物体的基础颜色
- Roughness:粗糙度。0=镜面光滑(像镜子),1=完全粗糙(像砂纸)
- Metallic:金属感。0=非金属,1=纯金属
- Clearcoat:清漆层。模拟物体表面的一层透明漆,让球看起来光滑圆润
2.7 场景管理器
最后,我们来写一个简单的场景管理器,负责初始化游戏场景。
using Godot;
/// <summary>
/// 场景管理器 - 负责初始化和管理游戏场景
/// </summary>
public partial class SceneManager : Node3D
{
/// <summary>桌面节点的引用</summary>
[Export] public Node3D Table { get; set; }
/// <summary>摄像机节点的引用</summary>
[Export] public CameraController Camera { get; set; }
/// <summary>灯光节点的引用</summary>
[Export] public DirectionalLight3D TableLight { get; set; }
/// <summary>所有球的容器节点</summary>
[Export] public Node3D BallsContainer { get; set; }
/// <summary>球桌内部尺寸(X方向)</summary>
private const float TableWidth = 2.54f;
/// <summary>球桌内部尺寸(Z方向)</summary>
private const float TableHeight = 1.27f;
/// <summary>桌面Y坐标</summary>
private const float TableY = 0.8f;
public override void _Ready()
{
GD.Print("桌球场景初始化中...");
// 验证所有必要的节点引用
if (Table == null)
{
GD.PrintErr("错误:未设置桌面节点引用!");
}
if (Camera == null)
{
GD.PrintErr("错误:未设置摄像机节点引用!");
}
if (TableLight == null)
{
GD.PrintErr("错误:未设置灯光节点引用!");
}
// 初始化摄像机位置
Camera?.ResetToDefault();
GD.Print("桌球场景初始化完成!");
GD.Print($"球桌尺寸: {TableWidth} x {TableHeight}");
}
/// <summary>
/// 获取球桌边界
/// </summary>
/// <returns>包含球桌最小和最大X、Z坐标的字典</returns>
public Dictionary<string, float> GetTableBounds()
{
return new Dictionary<string, float>
{
{ "min_x", -TableWidth / 2 },
{ "max_x", TableWidth / 2 },
{ "min_z", -TableHeight / 2 },
{ "max_z", TableHeight / 2 },
{ "y", TableY }
};
}
/// <summary>
/// 重置整个场景到初始状态
/// </summary>
public void ResetScene()
{
GD.Print("重置场景...");
Camera?.ResetToDefault();
}
}## 场景管理器 - 负责初始化和管理游戏场景
extends Node3D
## 桌面节点的引用
@export var table: Node3D
## 摄像机节点的引用
@export var camera: CameraController
## 灯光节点的引用
@export var table_light: DirectionalLight3D
## 所有球的容器节点
@export var balls_container: Node3D
## 球桌内部尺寸(X方向)
const TABLE_WIDTH: float = 2.54
## 球桌内部尺寸(Z方向)
const TABLE_HEIGHT: float = 1.27
## 桌面Y坐标
const TABLE_Y: float = 0.8
func _ready() -> void:
print("桌球场景初始化中...")
# 验证所有必要的节点引用
if table == null:
push_error("错误:未设置桌面节点引用!")
if camera == null:
push_error("错误:未设置摄像机节点引用!")
if table_light == null:
push_error("错误:未设置灯光节点引用!")
# 初始化摄像机位置
if camera:
camera.reset_to_default()
print("桌球场景初始化完成!")
print("球桌尺寸: %.2f x %.2f" % [TABLE_WIDTH, TABLE_HEIGHT])
## 获取球桌边界
func get_table_bounds() -> Dictionary:
return {
"min_x": -TABLE_WIDTH / 2,
"max_x": TABLE_WIDTH / 2,
"min_z": -TABLE_HEIGHT / 2,
"max_z": TABLE_HEIGHT / 2,
"y": TABLE_Y
}
## 重置整个场景到初始状态
func reset_scene() -> void:
print("重置场景...")
if camera:
camera.reset_to_default()2.8 目录结构建议
为了保持项目整洁,建议在文件系统中创建以下目录结构:
res://
├── scenes/ # 场景文件
│ ├── main.tscn # 主场景
│ ├── table.tscn # 球桌场景
│ └── ball.tscn # 球的场景(模板)
├── scripts/ # 脚本文件
│ ├── camera_controller.cs # 摄像机控制器
│ ├── scene_manager.cs # 场景管理器
│ ├── ball.cs # 球体脚本
│ ├── physics_manager.cs # 物理管理器
│ ├── aiming_controller.cs # 瞄准控制器
│ └── game_manager.cs # 游戏管理器
├── materials/ # 材质文件
│ ├── table_felt.tres # 台呢材质
│ ├── wood_frame.tres # 木框材质
│ └── cue_ball.tres # 母球材质
├── models/ # 3D模型(如果有的话)
├── audio/ # 音效文件
└── ui/ # UI场景
├── aim_line.tscn # 瞄准线
├── power_bar.tscn # 力度条
└── scoreboard.tscn # 计分板整洁的项目 = 高效的开发
把不同类型的文件放在不同的文件夹里,就像把衣服、鞋子、书籍分别放在不同的柜子里。需要什么的时候,一找就能找到。
2.9 小结
在本章中,我们完成了项目的搭建和场景配置:
- 创建项目:新建Godot 4项目,配置基础设置
- 物理设置:关闭重力、提高物理精度
- 摄像机:设置2.5D俯视角,编写摄像机控制器
- 球桌:搭建桌面、边框、球袋
- 灯光:环境光+台球灯+补光
- 材质:台呢、木框、球袋、球的材质
- 场景管理器:初始化和管理游戏场景
现在我们有了一个漂亮的球桌场景!接下来,我们将为球添加物理属性,让它们能真实地滚动和碰撞。
