13. 2.5D场景搭建
2.5D场景搭建
什么是场景
想象你在拍一部电影。每一场戏都有自己的"布景"——有舞台、灯光、道具、演员。在 Godot 里,场景(Scene) 就是游戏里的一个"布景"。
一个场景可以是:
- 一个游戏关卡(比如"第一关")
- 一个角色(比如"玩家"或"敌人")
- 一个 UI 界面(比如"主菜单"或"暂停界面")
场景的神奇之处
场景可以嵌套使用。就像电影里的"道具"可以在多个场景里反复出现,Godot 里的场景也可以被其他场景引用——这叫做实例化(Instantiate)。
Godot 的场景系统:节点树
场景里的所有东西都是节点(Node)。节点之间有父子关系,就像一棵倒过来的树:
- 最顶上的是根节点(Root Node)
- 根节点下面可以有多个子节点
- 子节点还可以有自己的子节点
这种结构叫做场景树(Scene Tree)。
父子关系的影响
父节点移动时,所有子节点会跟着一起移动。这是 Godot 场景系统的核心规则,要牢记!
2.5D 场景的典型结构
2.5D 游戏使用 3D 技术,但摄像机固定在某个角度,让游戏看起来像 2D。典型的 2.5D 场景包含以下节点:
根节点:Node3D
整个场景的容器,所有其他节点都放在它里面。
摄像机:Camera3D
2.5D 游戏的摄像机通常固定在一个角度:
- 横版游戏(如魂斗罗):摄像机在侧面,只能看到左右方向
- 俯视游戏(如3D割草):摄像机在上方,俯视整个场景
- 斜45度(如很多 RPG):摄像机斜向俯视
光源:DirectionalLight3D
DirectionalLight3D 就像太阳——它从一个方向照射,没有具体位置,整个场景都受它影响。
设置建议:
- 旋转 X 轴约 -45 度,模拟斜射的阳光
- 开启阴影(Shadow → Enabled)
环境:WorldEnvironment
控制场景的整体氛围:天空颜色、雾效、环境光等。
地面/平台:StaticBody3D
StaticBody3D 是静止不动的物理物体,适合做地面、墙壁、平台。
它需要两个子节点:
MeshInstance3D:显示外观(方块、平面等)CollisionShape3D:定义碰撞范围
UI 层:CanvasLayer
CanvasLayer 是专门放 2D UI 的容器。血条、得分、小地图都放在这里。
为什么 UI 要单独放在 CanvasLayer?
因为 UI 不应该随着摄像机移动而移动。CanvasLayer 会让 UI 始终固定在屏幕上,不受 3D 摄像机影响。
场景组织策略
一个完整的游戏通常有多个场景文件,合理组织它们非常重要。
推荐的场景文件结构
scenes/
├── Main.tscn # 主场景(游戏入口)
├── levels/
│ ├── Level1.tscn # 第一关
│ ├── Level2.tscn # 第二关
│ └── Level3.tscn # 第三关
├── characters/
│ ├── Player.tscn # 玩家角色(预制体)
│ └── Enemy.tscn # 敌人(预制体)
├── ui/
│ ├── HUD.tscn # 游戏内 UI(血条、得分)
│ ├── MainMenu.tscn # 主菜单
│ └── PauseMenu.tscn # 暂停菜单
└── effects/
└── Explosion.tscn # 爆炸特效(预制体)什么是预制体(Prefab)
预制体就是"可以反复使用的场景模板"。比如你做了一个 Enemy.tscn,游戏里需要 100 个敌人,你不需要手动创建 100 次——只需要把 Enemy.tscn 实例化 100 次就行了。
预制体的好处
修改预制体文件,所有实例都会自动更新。比如你修改了 Enemy.tscn 里的血量,游戏里所有敌人的血量都会跟着变。
主场景的作用
Main.tscn 是游戏启动时加载的第一个场景,通常负责:
- 显示主菜单
- 加载第一关
- 管理全局状态(音量、存档等)
场景实例化
实例化(Instantiate) 就是"把一个场景文件复制一份放到当前场景里"。
在编辑器里实例化
- 在场景面板,点击链条图标(或按
Ctrl+Shift+A) - 选择要实例化的场景文件(比如
Enemy.tscn) - 点击"打开",敌人就出现在场景里了
用代码动态实例化
更常见的做法是用代码在游戏运行时动态创建实例:
using Godot;
public partial class GameLevel : Node3D
{
// 预加载敌人场景(在编辑器里拖拽赋值)
[Export] public PackedScene EnemyScene;
public override void _Ready()
{
// 在游戏开始时生成3个敌人
for (int i = 0; i < 3; i++)
{
SpawnEnemy(new Vector3(i * 3.0f, 0, 0));
}
}
// 在指定位置生成一个敌人
private void SpawnEnemy(Vector3 position)
{
// 实例化敌人场景
var enemy = EnemyScene.Instantiate<CharacterBody3D>();
// 设置位置
enemy.Position = position;
// 添加到当前场景
AddChild(enemy);
}
}extends Node3D
# 预加载敌人场景(在编辑器里拖拽赋值)
@export var enemy_scene: PackedScene
func _ready():
# 在游戏开始时生成3个敌人
for i in range(3):
spawn_enemy(Vector3(i * 3.0, 0, 0))
# 在指定位置生成一个敌人
func spawn_enemy(position: Vector3):
# 实例化敌人场景
var enemy = enemy_scene.instantiate()
# 设置位置
enemy.position = position
# 添加到当前场景
add_child(enemy)场景切换
游戏里从一个关卡跳到另一个关卡,就是场景切换。
基本场景切换
using Godot;
public partial class LevelManager : Node
{
// 切换到指定场景(传入场景文件路径)
public void ChangeScene(string scenePath)
{
// GetTree() 获取场景树,ChangeSceneToFile 切换场景
GetTree().ChangeSceneToFile(scenePath);
}
// 示例:切换到第二关
public void GoToLevel2()
{
ChangeScene("res://scenes/levels/Level2.tscn");
}
// 示例:返回主菜单
public void GoToMainMenu()
{
ChangeScene("res://scenes/ui/MainMenu.tscn");
}
}extends Node
# 切换到指定场景(传入场景文件路径)
func change_scene(scene_path: String):
# get_tree() 获取场景树,change_scene_to_file 切换场景
get_tree().change_scene_to_file(scene_path)
# 示例:切换到第二关
func go_to_level2():
change_scene("res://scenes/levels/Level2.tscn")
# 示例:返回主菜单
func go_to_main_menu():
change_scene("res://scenes/ui/MainMenu.tscn")res:// ## 定义?
res:// 是 Godot 里的特殊路径前缀,表示"项目根目录"。res://scenes/Level1.tscn 就是项目根目录下的 scenes/Level1.tscn 文件。
完整示例:带波次生成的关卡场景
下面是一个完整的关卡场景脚本,展示了场景实例化和场景切换的综合用法:
using Godot;
public partial class GameLevel : Node3D
{
[Export] public PackedScene EnemyScene;
private int _score = 0;
private int _enemiesRemaining = 0;
private int _currentWave = 0;
private const int TotalWaves = 3;
// 每波敌人数量
private readonly int[] _waveSizes = { 3, 5, 8 };
public override void _Ready()
{
StartNextWave();
}
// 开始下一波
private void StartNextWave()
{
if (_currentWave >= TotalWaves)
{
// 所有波次完成,切换到胜利场景
GetTree().ChangeSceneToFile("res://scenes/ui/VictoryScreen.tscn");
return;
}
int count = _waveSizes[_currentWave];
_enemiesRemaining = count;
_currentWave++;
GD.Print($"第 {_currentWave} 波开始,共 {count} 个敌人");
// 在随机位置生成敌人
for (int i = 0; i < count; i++)
{
float x = GD.Randf() * 20.0f - 10.0f; // -10 到 10 之间
SpawnEnemy(new Vector3(x, 0, 0));
}
}
private void SpawnEnemy(Vector3 position)
{
var enemy = EnemyScene.Instantiate<Node3D>();
enemy.Position = position;
AddChild(enemy);
}
// 当一个敌人被消灭时调用此方法
public void OnEnemyDefeated()
{
_score += 100;
_enemiesRemaining--;
GD.Print($"得分:{_score},剩余敌人:{_enemiesRemaining}");
if (_enemiesRemaining <= 0)
{
// 当前波次清空,开始下一波
StartNextWave();
}
}
}extends Node3D
@export var enemy_scene: PackedScene
var score: int = 0
var enemies_remaining: int = 0
var current_wave: int = 0
const TOTAL_WAVES = 3
# 每波敌人数量
var wave_sizes = [3, 5, 8]
func _ready():
start_next_wave()
# 开始下一波
func start_next_wave():
if current_wave >= TOTAL_WAVES:
# 所有波次完成,切换到胜利场景
get_tree().change_scene_to_file("res://scenes/ui/VictoryScreen.tscn")
return
var count = wave_sizes[current_wave]
enemies_remaining = count
current_wave += 1
print("第 %d 波开始,共 %d 个敌人" % [current_wave, count])
# 在随机位置生成敌人
for i in range(count):
var x = randf_range(-10.0, 10.0)
spawn_enemy(Vector3(x, 0, 0))
func spawn_enemy(position: Vector3):
var enemy = enemy_scene.instantiate()
enemy.position = position
add_child(enemy)
# 当一个敌人被消灭时调用此方法
func on_enemy_defeated():
score += 100
enemies_remaining -= 1
print("得分:%d,剩余敌人:%d" % [score, enemies_remaining])
if enemies_remaining <= 0:
# 当前波次清空,开始下一波
start_next_wave()小结
| 概念 | 说明 |
|---|---|
| 场景(Scene) | 游戏里的一个"布景",保存为 .tscn 文件 |
| 节点(Node) | 场景里的每一个元素 |
| 场景树 | 节点按父子关系组成的树形结构 |
| 预制体 | 可以反复实例化的场景文件 |
| 实例化 | 把场景文件复制一份放到当前场景 |
| 场景切换 | 用 ChangeSceneToFile 跳转到另一个场景 |
掌握了场景系统,你就掌握了 Godot 游戏开发的核心骨架。接下来,我们来创建游戏里最重要的角色——玩家!
