2. 项目搭建与2.5D摄像机
2. 项目搭建与2.5D摄像机
创建 Godot 4 项目
打开 Godot 4 编辑器,点击"新建项目",按以下配置创建:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| 项目名称 | VampireSurvivors3D | 便于识别 |
| 项目路径 | 你喜欢的任意位置 | 建议路径不含中文 |
| 渲染器 | Forward+ | 3D项目推荐使用 |
| 版本控制 | Git | 强烈建议开启 |
为什么选 Forward+ 渲染器?
Godot 4 提供三种渲染器:
- Forward+:画质最好,适合桌面平台,支持最多高级特效
- Mobile:适合手机平台
- Compatibility:兼容旧设备,性能最好但画质低
我们的割草游戏主要面向 PC,所以选 Forward+。如果你打算发布到手机,可以后续切换。
项目文件夹结构
创建完成后,在 Godot 的文件系统面板中,按以下结构创建文件夹:
res://
├── scenes/ # 场景文件
│ ├── player/ # 玩家相关场景
│ ├── enemies/ # 敌人相关场景
│ ├── weapons/ # 武器相关场景
│ ├── items/ # 道具相关场景
│ └── ui/ # UI 场景
├── scripts/ # 脚本文件
│ ├── player/ # 玩家脚本
│ ├── enemies/ # 敌人脚本
│ ├── weapons/ # 武器脚本
│ ├── systems/ # 系统脚本(生成器、对象池等)
│ └── ui/ # UI 脚本
├── assets/ # 资源文件
│ ├── models/ # 3D模型
│ ├── textures/ # 贴图
│ ├── sounds/ # 音效
│ └── fonts/ # 字体
└── data/ # 数据配置
├── weapons/ # 武器配置
└── enemies/ # 敌人配置配置正交俯视摄像机
这是整个 2.5D 方案的核心。我们要让摄像机从正上方往下看,使用正交投影(没有近大远小的效果)。
创建主场景
- 创建一个新的 3D 场景(Node3D),命名为
Main - 添加一个 Camera3D 节点作为 Main 的子节点
- 在 Inspector 面板中配置 Camera3D:
| 属性 | 设置值 | 说明 |
|---|---|---|
| Projection | Orthographic | 正交投影,没有透视变形 |
| Size | 20 | 摄像机可视范围大小(数值越大看到越多) |
| Position | (0, 15, 0) | 在玩家上方15个单位 |
| Rotation | (-90, 0, 0) | 朝正下方看 |
正交摄像机的 Size 属性决定了画面能看到多大范围,就像调整望远镜的倍率:
| Size 值 | 效果 | 适用场景 |
|---|---|---|
| 10 | 看到的范围小,角色显得大 | 强调近战打击感 |
| 15-20 | 适中范围,能看清角色也有视野 | 推荐:割草游戏 |
| 30+ | 看到的范围很大,角色显得小 | 战略游戏 |
摄像机跟随脚本
摄像机需要平滑地跟随玩家移动。下面是摄像机跟随脚本:
// CameraFollow.cs
// 挂载到 Camera3D 节点上
using Godot;
public partial class CameraFollow : Camera3D
{
// 跟随目标(玩家)
[Export] public NodePath TargetPath;
private Node3D _target;
// 跟随平滑度(值越小越平滑,越大越紧)
[Export] public float SmoothSpeed = 5.0f;
// 摄像机偏移(在玩家上方多高)
[Export] public Vector3 Offset = new Vector3(0, 15, 0);
public override void _Ready()
{
// 获取跟随目标
if (TargetPath != null)
{
_target = GetNode<Node3D>(TargetPath);
}
}
public override void _PhysicsProcess(double delta)
{
if (_target == null) return;
// 计算目标位置(玩家位置 + 偏移)
Vector3 targetPosition = _target.GlobalPosition + Offset;
// 平滑插值移动到目标位置
Vector3 newPosition = GlobalPosition.Lerp(
targetPosition,
(float)(SmoothSpeed * delta)
);
GlobalPosition = newPosition;
}
}# camera_follow.gd
# 挂载到 Camera3D 节点上
extends Camera3D
# 跟随目标(玩家)
@export var target_path: NodePath
var _target: Node3D
# 跟随平滑度(值越小越平滑,越大越紧)
@export var smooth_speed: float = 5.0
# 摄像机偏移(在玩家上方多高)
@export var offset: Vector3 = Vector3(0, 15, 0)
func _ready():
# 获取跟随目标
if target_path:
_target = get_node(target_path)
func _physics_process(delta):
if _target == null:
return
# 计算目标位置(玩家位置 + 偏移)
var target_position = _target.global_position + offset
# 平滑插值移动到目标位置
var new_position = global_position.lerp(
target_position,
smooth_speed * delta
)
global_position = new_positionLerp 是什么?
Lerp(Linear Interpolation,线性插值)就像在两个点之间画一条线,然后选择线上某个百分比的位置。比如从 A 点到 B 点做 50% 的 Lerp,就是取中间点。每帧都往目标位置走一小步,就产生了"平滑跟随"的效果。
输入映射配置
割草游戏的核心操作只需要方向键(或WASD)移动。在 Godot 中配置输入映射:
打开 项目 -> 项目设置 -> 输入映射,添加以下动作:
| 动作名称 | 按键绑定 | 说明 |
|---|---|---|
| move_up | W / 上箭头 | 向上移动(屏幕上方) |
| move_down | S / 下箭头 | 向下移动 |
| move_left | A / 左箭头 | 向左移动 |
| move_right | D / 右箭头 | 向右移动 |
| pause | Escape | 暂停游戏 |
重要提示
在正交俯视视角中,"上"对应的是 3D 世界的 -Z 方向,"下"对应 +Z 方向。这和 2D 游戏中"上"是 -Y 方向不同。配置输入映射时不需要关心这个,但在代码中处理移动方向时要注意。
输入读取工具类
为了在代码中方便获取移动方向,我们创建一个输入工具类:
// InputHelper.cs
// 静态工具类,用于读取移动输入
using Godot;
public static class InputHelper
{
/// <summary>
/// 获取8方向移动输入向量
/// 返回值已归一化(长度为1),对角线移动不会更快
/// </summary>
public static Vector2 GetMovementInput()
{
Vector2 input = Vector2.Zero;
if (Input.IsActionPressed("move_up")) input.Y -= 1;
if (Input.IsActionPressed("move_down")) input.Y += 1;
if (Input.IsActionPressed("move_left")) input.X -= 1;
if (Input.IsActionPressed("move_right")) input.X += 1;
// 归一化,防止对角线移动速度是水平的 √2 倍
return input.Normalized();
}
/// <summary>
/// 将2D输入向量转换为3D世界的XZ平面移动向量
/// </summary>
public static Vector3 ToXZMovement(Vector2 input)
{
// 在我们的俯视视角中:
// 输入的 X -> 3D 世界的 X
// 输入的 Y -> 3D 世界的 Z
return new Vector3(input.X, 0, input.Y);
}
}# input_helper.gd
# 静态工具类,用于读取移动输入(使用 autoload)
extends Node
## 获取8方向移动输入向量
## 返回值已归一化(长度为1),对角线移动不会更快
func get_movement_input() -> Vector2:
var input = Vector2.ZERO
if Input.is_action_pressed("move_up"): input.y -= 1
if Input.is_action_pressed("move_down"): input.y += 1
if Input.is_action_pressed("move_left"): input.x -= 1
if Input.is_action_pressed("move_right"): input.x += 1
# 归一化,防止对角线移动速度是水平的 √2 倍
return input.normalized()
## 将2D输入向量转换为3D世界的XZ平面移动向量
func to_xz_movement(input: Vector2) -> Vector3:
# 在我们的俯视视角中:
# 输入的 X -> 3D 世界的 X
# 输入的 Y -> 3D 世界的 Z
return Vector3(input.x, 0, input.y)场景结构总览
最终我们的主场景树结构如下:
Main (Node3D) ← 游戏主节点
├── Ground (MeshInstance3D) ← 地面网格
├── Camera3D (Camera3D) ← 正交俯视摄像机
│ └── [CameraFollow 脚本] ← 摄像机跟随逻辑
├── Player (CharacterBody3D) ← 玩家角色
│ ├── Model (Node3D) ← 角色3D模型
│ └── CollisionShape3D ← 碰撞形状
├── Enemies (Node3D) ← 敌人容器(管理所有敌人)
├── Projectiles (Node3D) ← 弹幕容器(管理所有飞行物)
├── Drops (Node3D) ← 掉落物容器(经验宝石等)
└── UI (CanvasLayer) ← UI 层
├── HUD ← 游戏界面(血条、计时器等)
├── LevelUpScreen ← 升级选择界面
└── PauseMenu ← 暂停菜单为什么用容器节点?
把同类对象放在一个容器节点下(如 Enemies、Projectiles),这样做有几个好处:
- 方便批量管理:可以一次清除所有敌人
- 避免场景树混乱:不会和玩家、UI 节点混在一起
- 性能统计:快速知道当前有多少敌人、弹幕
创建简单地面
为了让场景看起来不那么空,我们添加一个简单的地面:
// GroundManager.cs
// 挂载到 Ground 节点,程序化生成地面网格
using Godot;
public partial class GroundManager : Node3D
{
[Export] public float GroundSize = 100f; // 地面大小
[Export] public Color GroundColor = new Color(0.2f, 0.35f, 0.15f); // 草绿色
public override void _Ready()
{
CreateGround();
}
private void CreateGround()
{
// 创建一个平面网格
var planeMesh = new PlaneMesh();
planeMesh.Size = new Vector2(GroundSize, GroundSize);
// 创建材质
var material = new StandardMaterial3D();
material.AlbedoColor = GroundColor;
planeMesh.SurfaceSetMaterial(0, material);
// 应用到 MeshInstance3D
var meshInstance = GetNode<MeshInstance3D>("../Ground");
meshInstance.Mesh = planeMesh;
}
}# ground_manager.gd
# 挂载到 Ground 节点,程序化生成地面网格
extends Node3D
@export var ground_size: float = 100.0 # 地面大小
@export var ground_color: Color = Color(0.2, 0.35, 0.15) # 草绿色
func _ready():
create_ground()
func create_ground():
# 创建一个平面网格
var plane_mesh = PlaneMesh.new()
plane_mesh.size = Vector2(ground_size, ground_size)
# 创建材质
var material = StandardMaterial3D.new()
material.albedo_color = ground_color
plane_mesh.surface_set_material(0, material)
# 应用到 MeshInstance3D
var mesh_instance = get_node("../Ground") as MeshInstance3D
mesh_instance.mesh = plane_mesh项目设置检查清单
在开始写代码之前,确认以下项目设置已经正确配置:
| 检查项 | 路径 | 推荐值 |
|---|---|---|
| 渲染器 | 项目设置 -> 常规 | Forward+ |
| 窗口大小 | 项目设置 -> 显示 -> 窗口 | 1280x720 |
| 拉伸模式 | 项目设置 -> 显示 -> 窗口 -> 拉伸 | canvas_items |
| 拉伸比例 | 项目设置 -> 显示 -> 窗口 -> 拉伸 | keep |
| 输入映射 | 项目设置 -> 输入映射 | 见上表 |
| 物理帧率 | 项目设置 -> 常规 -> 物理 | 60 FPS |
窗口拉伸设置
stretch mode 设为 canvas_items 可以让 UI 在不同分辨率下正确缩放。这对于割草游戏很重要,因为玩家可能用各种分辨率的屏幕。
总结
本章我们完成了:
- 创建 Godot 4 项目,选择 Forward+ 渲染器
- 搭建文件夹结构,按功能分类组织
- 配置正交俯视摄像机,实现 2.5D 视角
- 配置输入映射,支持 WASD 和方向键
- 创建场景树结构,为后续开发打好地基
下一步
地基打好了,接下来让角色动起来!
→ 3. 角色移动
