2. 项目搭建
2026/4/14大约 5 分钟
塔防——项目搭建
上一章我们设计了塔防游戏的核心玩法,现在打开 Godot,把项目搭起来。
创建项目
- 打开 Godot 编辑器
- 点击 新建,填写项目信息:
- 项目名称:
tower_defense - 渲染器:选择
Forward+(默认)
- 项目名称:
- 点击 创建并编辑
项目设置
窗口大小
路径:项目 → 项目设置 → 常规 → 显示 → 窗口
| 设置项 | 值 |
|---|---|
| 视口宽度 | 1280 |
| 视口高度 | 720 |
| 拉伸模式 | canvas_items |
| 拉伸宽高比 | keep |
图层设置
路径:项目 → 项目设置 → 常规 → Layer Names → 2D Physics
塔防游戏至少需要以下碰撞层:
| 层编号 | 名称 | 用途 |
|---|---|---|
| 1 | tower | 防御塔 |
| 2 | enemy | 敌人 |
| 3 | projectile | 子弹/弹丸 |
| 4 | path | 路径区域 |
输入映射
路径:项目 → 项目设置 → 输入映射
| 动作名称 | 按键 | 说明 |
|---|---|---|
place_tower | 鼠标左键 | 放置防御塔 |
cancel | 鼠标右键 / Esc | 取消放置 |
upgrade_tower | 鼠标左键 | 升级选中的塔 |
speed_up | 空格 | 加速游戏 |
项目文件结构
tower_defense/
├── scenes/ # 场景文件
│ ├── main_menu.tscn # 主菜单
│ ├── game.tscn # 游戏主场景
│ ├── map.tscn # 地图场景
│ ├── towers/ # 防御塔场景
│ │ ├── arrow_tower.tscn # 箭塔
│ │ ├── cannon_tower.tscn # 炮塔
│ │ └── ice_tower.tscn # 冰塔
│ ├── enemies/ # 敌人场景
│ │ ├── basic_enemy.tscn # 普通敌人
│ │ ├── fast_enemy.tscn # 快速敌人
│ │ └── boss_enemy.tscn # Boss
│ └── ui/ # UI 场景
│ ├── hud.tscn # 游戏内信息栏
│ ├── tower_panel.tscn # 塔选择面板
│ └── game_over.tscn # 游戏结束界面
├── scripts/ # 脚本文件
│ ├── managers/ # 管理器
│ │ ├── game_manager.gd # 游戏主管理器
│ │ ├── wave_manager.gd # 波次管理器
│ │ ├── economy_manager.gd # 经济管理器
│ │ └── grid_manager.gd # 网格管理器
│ ├── towers/ # 塔脚本
│ │ └── tower_base.gd # 塔基类
│ ├── enemies/ # 敌人脚本
│ │ └── enemy_base.gd # 敌人基类
│ └── projectiles/ # 弹丸脚本
│ └── projectile_base.gd # 弹丸基类
├── resources/ # 资源文件
│ ├── tower_data/ # 塔数据
│ ├── enemy_data/ # 敌人数据
│ └── wave_data/ # 波次数据
├── assets/ # 素材资源
│ ├── sprites/ # 图片
│ ├── sounds/ # 音效
│ └── fonts/ # 字体
└── autoload/ # 自动加载(单例)为什么要分这么多文件夹
这不是过度设计。随着项目变大,如果所有文件都堆在一个文件夹里,你会花大量时间翻找文件。按功能分类是专业游戏开发的基本习惯。
创建游戏管理器(Autoload)
游戏管理器是整个游戏的大脑,负责协调各个子系统。
创建 Autoload 脚本
- 在
scripts/managers/下创建game_manager.cs - 在
项目 → 项目设置 → Autoload中添加:- 名称:
GameManager - 路径:
res://scripts/managers/game_manager.cs
- 名称:
C
using Godot;
/// <summary>
/// 游戏主管理器——整个游戏的总指挥。
/// 就像一个公司的 CEO,不干具体的活,但负责协调各个部门。
/// </summary>
public partial class GameManager : Node
{
// 单例模式:让其他脚本可以通过 GameManager.Instance 访问
public static GameManager Instance { get; private set; }
// 游戏状态
public enum GameState
{
Menu, // 主菜单
Playing, // 游戏中
Paused, // 暂停
GameOver, // 游戏结束
Victory // 通关胜利
}
[Signal]
public delegate void StateChangedEventHandler(GameState newState);
[Signal]
public delegate void LivesChangedEventHandler(int lives);
private int _lives = 20; // 生命值
private int _gold = 200; // 初始金币
private int _wave = 0; // 当前波次
private GameState _state = GameState.Menu;
/// <summary>生命值</summary>
public int Lives
{
get => _lives;
set
{
_lives = Mathf.Max(0, value);
EmitSignal(SignalName.LivesChanged, _lives);
if (_lives <= 0 && _state == GameState.Playing)
ChangeState(GameState.GameOver);
}
}
/// <summary>金币</summary>
public int Gold
{
get => _gold;
set => _gold = Mathf.Max(0, value);
}
/// <summary>当前波次</summary>
public int Wave => _wave;
public override void _Ready()
{
if (Instance != null && Instance != this)
{
QueueFree(); // 防止重复创建
return;
}
Instance = this;
}
/// <summary>切换游戏状态</summary>
public void ChangeState(GameState newState)
{
_state = newState;
EmitSignal(SignalName.StateChanged, newState);
// 根据状态暂停/恢复游戏树
GetTree().Paused = (newState == GameState.Paused);
}
/// <summary>开始新游戏</summary>
public void StartGame()
{
_lives = 20;
_gold = 200;
_wave = 0;
ChangeState(GameState.Playing);
}
/// <summary>进入下一波</summary>
public void NextWave()
{
_wave++;
}
}GDScript
extends Node
## 游戏主管理器——整个游戏的总指挥。
## 就像一个公司的 CEO,不干具体的活,但负责协调各个部门。
# 单例模式:让其他脚本可以通过 GameManager 访问
@onready var instance: Node = self
# 游戏状态
enum GameState { MENU, PLAYING, PAUSED, GAME_OVER, VICTORY }
signal state_changed(new_state: GameState)
signal lives_changed(lives: int)
var lives: int = 20 # 生命值
var gold: int = 200 # 初始金币
var wave: int = 0 # 当前波次
var state: GameState = GameState.MENU
func _ready():
# 防止重复创建
if instance and instance != self:
queue_free()
return
instance = self
## 切换游戏状态
func change_state(new_state: GameState) -> void:
state = new_state
state_changed.emit(new_state)
# 根据状态暂停/恢复游戏树
get_tree().paused = (new_state == GameState.PAUSED)
## 开始新游戏
func start_game() -> void:
lives = 20
gold = 200
wave = 0
change_state(GameState.PLAYING)
## 进入下一波
func next_wave() -> void:
wave += 1创建游戏主场景
游戏主场景 game.tscn 的节点结构:
Game (Node2D) ← 游戏主场景根节点
├── Map (Node2D) ← 地图(TileMap + 路径)
├── TowerContainer (Node2D) ← 存放所有塔的容器
├── EnemyContainer (Node2D) ← 存放所有敌人的容器
├── ProjectileContainer (Node2D) ← 存放所有弹丸的容器
└── HUD (CanvasLayer) ← UI 层(始终在最前面)
├── TopBar (HBoxContainer) ← 顶部信息栏(生命、金币、波次)
├── TowerPanel (PanelContainer) ← 塔选择面板
└── WaveInfo (Label) ← 波次信息提示为什么用容器节点
TowerContainer、EnemyContainer 这些节点本身不做任何事情,它们只是一个"文件夹",用来组织同类节点。这样在代码中你可以用 GetNode("TowerContainer").GetChildren() 快速获取所有塔。
创建基础场景模板
现在创建几个最基础的场景骨架,后续章节会逐步完善。
敌人基础场景
BasicEnemy (CharacterBody2D)
├── Sprite2D ← 敌人图片
├── CollisionShape2D ← 碰撞形状
├── HealthBar (Control) ← 血条
│ └── ProgressBar ← 进度条
└── NavigationAgent2D ← 导航代理(沿路径移动)塔基础场景
ArrowTower (StaticBody2D)
├── Sprite2D ← 塔图片
├── CollisionShape2D ← 碰撞形状
├── RangeIndicator (Area2D) ← 攻击范围
│ └── CollisionShape2D ← 范围碰撞形状
└── Timer ← 攻击间隔计时器下一章
项目骨架搭好了,接下来画地图、定路径。
