1. 核心玩法设计
1. 保卫萝卜——核心玩法
本章你将掌握
通过本章学习,你将理解并掌握以下核心知识:
- 塔防游戏的本质:理解"放置-自动攻击-资源管理"的核心循环
- 金币经济系统:掌握资源获取、消耗和平衡的设计原则
- 波次系统设计:学会如何设计递增难度和节奏感
- 游戏状态管理:使用枚举和状态机管理游戏流程
- 信号系统应用:通过信号实现模块间的解耦通信
- 场景组织架构:理解塔防游戏的节点层级结构
这些知识将为你后续实现完整的塔防游戏打下坚实基础。
1.1 什么是塔防游戏?
想象一下:你家门口有一条小路,一群不速之客正沿着这条路朝你家走来,要抢走你最心爱的萝卜。你能做什么?在路两边摆上各种"机关"来阻止它们!这就是塔防游戏的核心思想。
塔防游戏就像是在玩一场"阵地保卫战":
| 要素 | 生活中的比喻 | 游戏中的对应 |
|---|---|---|
| 敌人 | 想偷你萝卜的小偷 | 沿路径行走的怪物 |
| 防御塔 | 你在路边设的"关卡" | 玩家放置的攻击单位 |
| 金币 | 你的"预算" | 放塔和升级需要花的钱 |
| 萝卜 | 你要保护的东西 | 游戏的终点/基地 |
| 生命值 | 萝卜的"健康" | 萝卜被怪物碰到就减少 |
1.2 保卫萝卜的核心玩法循环
整个游戏可以用一个简单的循环来概括:
放置防御塔 → 怪物沿路走 → 塔自动攻击怪物 → 怪物被打死或到达萝卜让我们把这个循环拆开来看每一个环节。
第一步:放置防御塔
玩家用金币在路径旁边放置不同类型的防御塔。每种塔有不同的特点——有的攻击快但伤害低,有的攻击慢但伤害高,有的能同时攻击多个敌人。
第二步:怪物沿路径行走
怪物会沿着一条固定的路线(就像走在人行道上一样)朝萝卜的方向前进。玩家不能改变怪物的路线,但可以通过合理放置防御塔来最大化输出。
第三步:防御塔自动攻击
当怪物进入防御塔的攻击范围时,塔会自动对怪物开火。玩家不需要手动操作攻击,只需要关注"在哪里放什么塔"这个策略问题。
第四步:结果判定
- 怪物被打死 → 玩家获得金币奖励
- 怪物到达萝卜 → 玩家失去生命值
游戏结束条件
| 条件 | 结果 |
|---|---|
| 生命值降到 0 | 游戏失败,萝卜被偷走了 |
| 所有波次的怪物都被消灭 | 游戏胜利,萝卜安全了 |
1.3 金币经济系统
金币是游戏中最核心的资源。你可以把它理解为"预算"——你需要在有限的预算内做出最优的决策。
金币的来源和用途
来源 用途
├── 初始金币(每关开始时发放) ├── 购买防御塔(每个塔价格不同)
├── 击杀怪物奖励 ├── 升级已有的防御塔
└── 出售防御塔(返还部分金币) └── (有时候需要"攒钱"等更好的塔)金币经济的设计原则
好的塔防游戏,金币经济要满足以下平衡:
| 原则 | 说明 | 示例 |
|---|---|---|
| 刚开始够用 | 初始金币至少能放 2-3 个基础塔 | 初始 300 金,基础塔 100 金 |
| 不能无限放 | 金币获取速度要低于"全铺满"的速度 | 高级塔要 200-500 金 |
| 击杀有回报 | 打死怪物能让你继续投入 | 击杀奖励 10-50 金 |
| 升级有价值 | 升级比买新塔更划算 | 升级费用递增但效果更好 |
1.4 波次系统
怪物不是一次性全部涌来的,而是分成一波一波的,就像海浪一样一波接一波。
什么是"波次"?
波次就是"第几批怪物"。每一波怪物的数量和强度都不一样,越往后越难。
| 波次特征 | 说明 |
|---|---|
| 数量递增 | 第 1 波 5 只,第 5 波可能 15 只 |
| 血量递增 | 后面波次的怪物更"皮实" |
| 速度递增 | 后面的怪物走得更快 |
| 出现 Boss | 每隔几波会出现一个特别强的 Boss |
| 间隔时间 | 两波之间有短暂休息时间 |
波次设计的节奏感
好的波次设计就像音乐一样有节奏感:
第1波:简单 → 玩家熟悉操作
第2波:稍难 → 玩家开始思考策略
第3波:中等 → 玩家需要升级塔
第4波:困难 → 玩家需要合理布局
第5波:Boss → 考验玩家前期积累
第6波:喘息 → 给玩家喘息和调整的时间
...重复这个节奏,难度逐步上升1.5 核心数据结构设计
在开始编码之前,我们先定义游戏中的核心数据。你可以把"数据结构"理解为游戏的"骨架"——它决定了游戏中有什么东西、每样东西有哪些属性。
怪物数据
// 怪物的核心属性(伪代码,帮助理解)
Monster = {
name: "小兵", // 名字
hp: 100, // 血量(能挨几下打)
speed: 2.0, // 移动速度(每秒走多远)
reward: 20, // 被打死奖励的金币
damage: 1, // 到达萝卜时扣多少生命
}防御塔数据
// 防御塔的核心属性
Tower = {
name: "瓶子塔", // 名字
cost: 100, // 购买价格
damage: 10, // 每次攻击伤害
range: 150.0, // 攻击范围(像手电筒能照多远)
fireRate: 1.0, // 每秒攻击几次
level: 1, // 当前等级
}游戏状态
// 游戏的"状态"就是游戏当前的"情况"
GameState = {
gold: 300, // 当前金币
lives: 10, // 剩余生命值
wave: 1, // 当前第几波
score: 0, // 分数
isPlaying: true, // 游戏是否在进行中
}1.6 用代码实现游戏管理器
游戏管理器(GameManager)就像是"总指挥",它负责记录游戏的状态、控制游戏的流程。每个游戏都需要一个这样的"总指挥"来协调整个游戏的运转。
游戏状态枚举
首先,我们定义游戏可能处于的几种"状态"——就像交通灯有红、黄、绿三种状态一样。
/// <summary>
/// 游戏状态枚举
/// 就像交通灯有红黄绿一样,游戏也有不同的"阶段"
/// </summary>
public enum GameState
{
/// <summary>准备阶段:还没开始打</summary>
Ready,
/// <summary>战斗阶段:怪物正在来</summary>
Fighting,
/// <summary>波次间歇:这一波打完了,下一波还没来</summary>
WaveBreak,
/// <summary>胜利:所有怪物都消灭了</summary>
Victory,
/// <summary>失败:萝卜被偷走了</summary>
Defeat
}## 游戏状态枚举
## 就像交通灯有红黄绿一样,游戏也有不同的"阶段"
enum GameState {
READY, ## 准备阶段:还没开始打
FIGHTING, ## 战斗阶段:怪物正在来
WAVE_BREAK, ## 波次间歇:这一波打完了,下一波还没来
VICTORY, ## 胜利:所有怪物都消灭了
DEFEAT ## 失败:萝卜被偷走了
}游戏管理器基础实现
接下来实现游戏管理器的核心逻辑。这个脚本会挂载到一个叫 GameManager 的节点上。
using Godot;
/// <summary>
/// 游戏管理器 —— 游戏的"总指挥"
/// 负责管理金币、生命值、波次、分数等核心数据
/// </summary>
public partial class GameManager : Node
{
// ===== 信号(事件通知) =====
// 信号就像"广播",当某件事发生时通知其他节点
/// <summary>金币数量变化时发送</summary>
[Signal]
public delegate void GoldChangedEventHandler(int newGold);
/// <summary>生命值变化时发送</summary>
[Signal]
public delegate void LivesChangedEventHandler(int newLives);
/// <summary>波次变化时发送</summary>
[Signal]
public delegate void WaveChangedEventHandler(int newWave);
/// <summary>游戏状态变化时发送</summary>
[Signal]
public delegate void StateChangedEventHandler(GameState newState);
/// <summary>游戏结束(胜利或失败)时发送</summary>
[Signal]
public delegate void GameOverEventHandler(bool isVictory);
// ===== 游戏配置参数 =====
// 这些是可以调整的"旋钮",用来平衡游戏难度
[ExportGroup("Game Config")]
[Export] public int StartingGold { get; set; } = 300;
[Export] public int StartingLives { get; set; } = 10;
[Export] public int TotalWaves { get; set; } = 20;
[Export] public float WaveBreakDuration { get; set; } = 10.0f;
// ===== 运行时状态 =====
// 这些是游戏运行过程中会变化的数据
private int _gold;
private int _lives;
private int _currentWave;
private int _score;
private GameState _state;
// ===== 只读属性(外部可以看,但不能直接改) =====
public int Gold => _gold;
public int Lives => _lives;
public int CurrentWave => _currentWave;
public int Score => _score;
public GameState State => _state;
public override void _Ready()
{
// 游戏启动时初始化所有数据
InitGame();
}
/// <summary>
/// 初始化游戏 —— 把所有数据恢复到初始状态
/// 就像按下"重启"按钮一样
/// </summary>
public void InitGame()
{
_gold = StartingGold;
_lives = StartingLives;
_currentWave = 0;
_score = 0;
_state = GameState.Ready;
// 通知 UI 更新显示
EmitSignal(SignalName.GoldChanged, _gold);
EmitSignal(SignalName.LivesChanged, _lives);
EmitSignal(SignalName.WaveChanged, _currentWave);
EmitSignal(SignalName.StateChanged, (int)_state);
}
/// <summary>
/// 增加金币 —— 怪物被击杀时调用
/// </summary>
public void AddGold(int amount)
{
_gold += amount;
EmitSignal(SignalName.GoldChanged, _gold);
}
/// <summary>
/// 花费金币 —— 购买或升级防御塔时调用
/// 如果金币不够,返回 false 表示购买失败
/// </summary>
public bool SpendGold(int amount)
{
if (_gold < amount)
{
return false; // 钱不够,买不了
}
_gold -= amount;
EmitSignal(SignalName.GoldChanged, _gold);
return true; // 扣钱成功
}
/// <summary>
/// 减少生命值 —— 怪物到达萝卜时调用
/// </summary>
public void LoseLife(int damage = 1)
{
_lives -= damage;
// 生命值不能低于 0
if (_lives < 0) _lives = 0;
EmitSignal(SignalName.LivesChanged, _lives);
// 检查游戏是否结束
if (_lives <= 0)
{
OnGameOver(false);
}
}
/// <summary>
/// 开始下一波 —— 玩家点击"开始下一波"按钮时调用
/// </summary>
public void StartNextWave()
{
if (_state != GameState.Ready && _state != GameState.WaveBreak)
return; // 当前状态不允许开始新波次
_currentWave++;
_state = GameState.Fighting;
EmitSignal(SignalName.WaveChanged, _currentWave);
EmitSignal(SignalName.StateChanged, (int)_state);
}
/// <summary>
/// 当前波次结束 —— 所有怪物都被消灭时调用
/// </summary>
public void OnWaveComplete()
{
if (_currentWave >= TotalWaves)
{
// 最后一波打完了,游戏胜利!
OnGameOver(true);
}
else
{
// 进入波次间歇,让玩家准备一下
_state = GameState.WaveBreak;
EmitSignal(SignalName.StateChanged, (int)_state);
}
}
/// <summary>
/// 增加分数
/// </summary>
public void AddScore(int points)
{
_score += points;
}
/// <summary>
/// 游戏结束处理
/// </summary>
private void OnGameOver(bool isVictory)
{
_state = isVictory ? GameState.Victory : GameState.Defeat;
EmitSignal(SignalName.StateChanged, (int)_state);
EmitSignal(SignalName.GameOver, isVictory);
}
}extends Node
class_name GameManager
## 游戏管理器 —— 游戏的"总指挥"
## 负责管理金币、生命值、波次、分数等核心数据
# ===== 信号(事件通知) =====
## 信号就像"广播",当某件事发生时通知其他节点
## 金币数量变化时发送
signal gold_changed(new_gold: int)
## 生命值变化时发送
signal lives_changed(new_lives: int)
## 波次变化时发送
signal wave_changed(new_wave: int)
## 游戏状态变化时发送
signal state_changed(new_state: int)
## 游戏结束(胜利或失败)时发送
signal game_over(is_victory: bool)
# ===== 游戏配置参数 =====
## 这些是可以调整的"旋钮",用来平衡游戏难度
## 初始金币
@export var starting_gold: int = 300
## 初始生命值
@export var starting_lives: int = 10
## 总波次数
@export var total_waves: int = 20
## 波次间歇时间(秒)
@export var wave_break_duration: float = 10.0
# ===== 运行时状态 =====
## 这些是游戏运行过程中会变化的数据
var _gold: int = 0
var _lives: int = 0
var _current_wave: int = 0
var _score: int = 0
var _state: int = GameState.READY
# ===== 只读属性(外部可以看,但不能直接改) =====
var gold: int:
get: return _gold
var lives: int:
get: return _lives
var current_wave: int:
get: return _current_wave
var score: int:
get: return _score
var state: int:
get: return _state
func _ready() -> void:
# 游戏启动时初始化所有数据
init_game()
## 初始化游戏 —— 把所有数据恢复到初始状态
## 就像按下"重启"按钮一样
func init_game() -> void:
_gold = starting_gold
_lives = starting_lives
_current_wave = 0
_score = 0
_state = GameState.READY
# 通知 UI 更新显示
gold_changed.emit(_gold)
lives_changed.emit(_lives)
wave_changed.emit(_current_wave)
state_changed.emit(_state)
## 增加金币 —— 怪物被击杀时调用
func add_gold(amount: int) -> void:
_gold += amount
gold_changed.emit(_gold)
## 花费金币 —— 购买或升级防御塔时调用
## 如果金币不够,返回 false 表示购买失败
func spend_gold(amount: int) -> bool:
if _gold < amount:
return false # 钱不够,买不了
_gold -= amount
gold_changed.emit(_gold)
return true # 扣钱成功
## 减少生命值 —— 怪物到达萝卜时调用
func lose_life(damage: int = 1) -> void:
_lives -= damage
# 生命值不能低于 0
if _lives < 0:
_lives = 0
lives_changed.emit(_lives)
# 检查游戏是否结束
if _lives <= 0:
_on_game_over(false)
## 开始下一波 —— 玩家点击"开始下一波"按钮时调用
func start_next_wave() -> void:
if _state != GameState.READY and _state != GameState.WAVE_BREAK:
return # 当前状态不允许开始新波次
_current_wave += 1
_state = GameState.FIGHTING
wave_changed.emit(_current_wave)
state_changed.emit(_state)
## 当前波次结束 —— 所有怪物都被消灭时调用
func on_wave_complete() -> void:
if _current_wave >= total_waves:
# 最后一波打完了,游戏胜利!
_on_game_over(true)
else:
# 进入波次间歇,让玩家准备一下
_state = GameState.WAVE_BREAK
state_changed.emit(_state)
## 增加分数
func add_score(points: int) -> void:
_score += points
## 游戏结束处理
func _on_game_over(is_victory: bool) -> void:
if is_victory:
_state = GameState.VICTORY
else:
_state = GameState.DEFEAT
state_changed.emit(_state)
game_over.emit(is_victory)1.7 核心玩法的场景组织
Godot 中的"场景"就像是一个个独立的"舞台",每个舞台上演不同的戏。保卫萝卜的场景组织如下:
Main.tscn (主场景 —— 最大的舞台)
├── Map (地图层 —— 路径和背景)
│ ├── Path2D (怪物行走的路线)
│ ├── TileMap (地图格子 —— 用来标记哪里可以放塔)
│ └── Carrot (萝卜 —— 终点)
├── Monsters (怪物容器 —— 所有活着的怪物都在这里)
├── Towers (防御塔容器 —— 所有已放置的塔都在这里)
├── Projectiles (子弹容器 —— 所有飞行中的子弹都在这里)
├── Effects (特效容器 —— 爆炸、粒子等)
├── GameManager (游戏管理器 —— 总指挥)
├── WaveManager (波次管理器 —— 管理怪物出场的节奏)
└── UI (界面层 —— 显示金币、生命值等信息)
├── TopBar (顶部信息栏)
├── TowerPanel (塔选择面板)
└── GameOverPanel (结算面板)游戏核心节点
点击节点可跳转到对应文档查看详细说明。
1.8 本节小结
在这一节中,我们学习了保卫萝卜这个塔防游戏的核心设计:
| 概念 | 说明 |
|---|---|
| 核心循环 | 放塔 → 怪物走路 → 塔攻击 → 判定结果 |
| 金币经济 | 有限的资源,需要合理分配 |
| 波次系统 | 怪物分批出现,难度递增 |
| 游戏状态 | 准备、战斗、间歇、胜利、失败 |
| GameManager | 总指挥,管理所有核心数据 |
在下一节中,我们将正式搭建项目,创建这些场景节点,并让 GameManager 在 Godot 中运行起来。
