4. 防御塔系统
2026/4/14大约 11 分钟
塔防——防御塔系统
地图画好了,路径也定义好了,现在该放"武器"了。防御塔就是你的武器——它们站在路边,看到敌人进入射程就自动开火。
本章你将学会:塔怎么自动发现敌人、怎么瞄准、怎么发射子弹。
塔的工作原理
防御塔的工作可以拆成三个步骤,就像一个自动售货机:
- 检测:扫描周围有没有敌人(就像售货机扫描你投的硬币)
- 瞄准:让炮口对准最近的敌人(就像售货机识别你选的商品)
- 攻击:发射子弹(就像售货机掉出饮料)
这个循环不断重复,直到敌人走出射程或者被消灭。
塔的节点结构
ArrowTower (StaticBody2D) ← 塔的主体(静态,不会移动)
├── Sprite2D ← 塔的底座图片
├── Turret (Sprite2D) ← 炮台/枪管(可以旋转指向敌人)
├── CollisionShape2D ← 塔的碰撞体积
├── DetectionArea (Area2D) ← 攻击范围检测区域
│ └── CollisionShape2D ← 圆形碰撞形状(半径 = 攻击范围)
├── AttackTimer (Timer) ← 攻击间隔计时器
└── RangeVisual (Sprite2D) ← 射程显示圈(放置时才显示)为什么用 StaticBody2D 而不是 CharacterBody2D
塔不需要移动,所以用 StaticBody2D(静态物体)。CharacterBody2D 是给会动的角色用的,塔放着不动,用静态物体更省性能。
塔的基类代码
所有类型的塔都有一些共同行为(检测敌人、攻击间隔、放置逻辑),所以我们先写一个"基类",然后让具体的塔继承它。
什么是基类和继承
基类就像一份"模板"。箭塔、炮塔、冰塔都是塔,它们有很多相同的功能。我们把这些相同的功能写在基类里,具体的塔只需要写自己独特的部分。这就像你做了一个蛋糕模板(基类),巧克力蛋糕、草莓蛋糕、抹茶蛋糕(子类)都在模板基础上加不同的味道。
C
using Godot;
/// <summary>
/// 防御塔基类——所有塔的通用逻辑。
/// 具体的塔(箭塔、炮塔、冰塔)继承这个类,只需要修改自己的特殊属性。
/// </summary>
public partial class TowerBase : StaticBody2D
{
[ExportGroup("塔属性")]
[Export] public float AttackRange { get; set; } = 150.0f; // 攻击范围(像素)
[Export] public float AttackInterval { get; set; } = 1.0f; // 攻击间隔(秒)
[Export] public int Damage { get; set; } = 10; // 每次攻击伤害
[Export] public int TowerCost { get; set; } = 50; // 放置花费
[ExportGroup("升级属性")]
[Export] public int MaxLevel { get; set; } = 3; // 最大等级
[Export] public int UpgradeCost { get; set; } = 30; // 升级花费
// 节点引用
protected Sprite2D _turret; // 炮台(可以旋转)
protected Area2D _detectionArea; // 检测区域
protected Timer _attackTimer; // 攻击计时器
protected Sprite2D _rangeVisual; // 射程显示圈
// 运行时状态
protected int _level = 1; // 当前等级
protected Node2D _currentTarget; // 当前攻击目标
protected readonly Godot.Collections.Array<Node2D> _enemiesInRange = new();
public override void _Ready()
{
_turret = GetNode<Sprite2D>("Turret");
_detectionArea = GetNode<Area2D>("DetectionArea");
_attackTimer = GetNode<Timer>("AttackTimer");
_rangeVisual = GetNode<Sprite2D>("RangeVisual");
// 设置攻击间隔
_attackTimer.WaitTime = AttackInterval;
_attackTimer.Timeout += OnAttackTimerTimeout;
// 设置射程圈大小
float diameter = AttackRange * 2;
_rangeVisual.Scale = new Vector2(diameter / 64, diameter / 64);
_rangeVisual.Visible = false; // 默认不显示
// 监听检测区域内的敌人进出
_detectionArea.BodyEntered += OnEnemyEntered;
_detectionArea.BodyExited += OnEnemyExited;
}
/// <summary>敌人进入攻击范围</summary>
private void OnEnemyEntered(Node2D body)
{
if (body is EnemyBase enemy)
{
_enemiesInRange.Add(enemy);
// 如果没有目标,立刻锁定这个敌人
if (_currentTarget == null)
_currentTarget = enemy;
}
}
/// <summary>敌人离开攻击范围</summary>
private void OnEnemyExited(Node2D body)
{
if (body is EnemyBase enemy)
{
_enemiesInRange.Remove(enemy);
// 如果离开的是当前目标,切换到下一个
if (_currentTarget == enemy)
_currentTarget = _enemiesInRange.Count > 0
? _enemiesInRange[0] : null;
}
}
/// <summary>攻击计时器到期</summary>
private void OnAttackTimerTimeout()
{
// 清理已死亡的敌人引用
_enemiesInRange.RemoveAll(e => !IsInstanceValid(e) || e.IsQueuedForDeletion());
if (_currentTarget == null || !IsInstanceValid(_currentTarget))
{
_currentTarget = _enemiesInRange.Count > 0
? _enemiesInRange[0] : null;
}
if (_currentTarget != null)
PerformAttack();
}
/// <summary>执行攻击(子类重写此方法实现不同的攻击方式)</summary>
protected virtual void PerformAttack()
{
// 瞄准目标:让炮台朝向敌人
if (_currentTarget != null)
{
float angle = GlobalPosition.AngleToPoint(_currentTarget.GlobalPosition);
_turret.Rotation = angle;
}
}
/// <summary>显示/隐藏射程圈</summary>
public void ShowRange(bool show)
{
_rangeVisual.Visible = show;
}
/// <summary>升级塔</summary>
public virtual void Upgrade()
{
if (_level >= MaxLevel) return;
if (GameManager.Instance.Gold >= UpgradeCost)
{
GameManager.Instance.Gold -= UpgradeCost;
_level++;
// 升级后增强属性
Damage = (int)(Damage * 1.3f);
AttackRange *= 1.1f;
}
}
}GDScript
extends StaticBody2D
## 防御塔基类——所有塔的通用逻辑。
## 具体的塔(箭塔、炮塔、冰塔)继承这个类,只需要修改自己的特殊属性。
@export_group("塔属性")
@export var attack_range: float = 150.0 # 攻击范围(像素)
@export var attack_interval: float = 1.0 # 攻击间隔(秒)
@export var damage: int = 10 # 每次攻击伤害
@export var tower_cost: int = 50 # 放置花费
@export_group("升级属性")
@export var max_level: int = 3 # 最大等级
@export var upgrade_cost: int = 30 # 升级花费
@onready var turret: Sprite2D = $Turret
@onready var detection_area: Area2D = $DetectionArea
@onready var attack_timer: Timer = $AttackTimer
@onready var range_visual: Sprite2D = $RangeVisual
var level: int = 1 # 当前等级
var current_target: Node2D = null # 当前攻击目标
var enemies_in_range: Array[Node2D] = []
func _ready():
# 设置攻击间隔
attack_timer.wait_time = attack_interval
attack_timer.timeout.connect(_on_attack_timer_timeout)
# 设置射程圈大小
var diameter = attack_range * 2
range_visual.scale = Vector2(diameter / 64.0, diameter / 64.0)
range_visual.visible = false # 默认不显示
# 监听检测区域内的敌人进出
detection_area.body_entered.connect(_on_enemy_entered)
detection_area.body_exited.connect(_on_enemy_exited)
## 敌人进入攻击范围
func _on_enemy_entered(body: Node2D):
if body is EnemyBase:
enemies_in_range.append(body)
if current_target == null:
current_target = body
## 敌人离开攻击范围
func _on_enemy_exited(body: Node2D):
if body is EnemyBase:
enemies_in_range.erase(body)
if current_target == body:
current_target = enemies_in_range[0] if enemies_in_range.size() > 0 else null
## 攻击计时器到期
func _on_attack_timer_timeout():
# 清理已死亡的敌人引用
enemies_in_range = enemies_in_range.filter(func(e): return is_instance_valid(e) and not e.is_queued_for_deletion())
if current_target == null or not is_instance_valid(current_target):
current_target = enemies_in_range[0] if enemies_in_range.size() > 0 else null
if current_target != null:
perform_attack()
## 执行攻击(子类重写此方法实现不同的攻击方式)
func perform_attack():
# 瞄准目标:让炮台朝向敌人
if current_target != null:
var angle = global_position.angle_to_point(current_target.global_position)
turret.rotation = angle
## 显示/隐藏射程圈
func show_range(show: bool):
range_visual.visible = show
## 升级塔
func upgrade():
if level >= max_level:
return
if GameManager.instance.gold >= upgrade_cost:
GameManager.instance.gold -= upgrade_cost
level += 1
# 升级后增强属性
damage = int(damage * 1.3)
attack_range *= 1.1箭塔——单体快速攻击
箭塔是最基础的塔:攻速快、伤害低、只打一个敌人。
C
using Godot;
/// <summary>
/// 箭塔——发射箭矢攻击单个敌人。
/// 优点:攻速快,适合对付少量强敌
/// 缺点:只能打一个目标
/// </summary>
public partial class ArrowTower : TowerBase
{
[Export] public PackedScene ArrowScene { get; set; }
public ArrowTower()
{
AttackRange = 180.0f;
AttackInterval = 0.6f; // 攻速快:每 0.6 秒射一次
Damage = 8;
TowerCost = 50;
}
protected override void PerformAttack()
{
base.PerformAttack(); // 调用基类的瞄准逻辑
if (_currentTarget == null || ArrowScene == null) return;
// 生成箭矢
var arrow = ArrowScene.Instantiate<ProjectileBase>();
arrow.GlobalPosition = _turret.GlobalPosition;
arrow.Damage = Damage;
arrow.Target = _currentTarget;
// 把箭矢添加到弹丸容器中
var container = GetNode<Node2D>("/root/Game/ProjectileContainer");
container.AddChild(arrow);
}
}GDScript
extends "res://scripts/towers/tower_base.gd"
## 箭塔——发射箭矢攻击单个敌人。
## 优点:攻速快,适合对付少量强敌
## 缺点:只能打一个目标
@export var arrow_scene: PackedScene
func _init():
attack_range = 180.0
attack_interval = 0.6 # 攻速快:每 0.6 秒射一次
damage = 8
tower_cost = 50
func perform_attack():
super.perform_attack() # 调用基类的瞄准逻辑
if current_target == null or arrow_scene == null:
return
# 生成箭矢
var arrow = arrow_scene.instantiate()
arrow.global_position = turret.global_position
arrow.damage = damage
arrow.target = current_target
# 把箭矢添加到弹丸容器中
var container = get_node("/root/Game/ProjectileContainer")
container.add_child(arrow)炮塔——范围伤害
炮塔射速慢,但爆炸后可以伤害一片区域内的所有敌人。
C
using Godot;
/// <summary>
/// 炮塔——发射炮弹,命中后产生范围爆炸。
/// 优点:范围伤害,适合对付密集的敌人
/// 缺点:攻速慢
/// </summary>
public partial class CannonTower : TowerBase
{
[Export] public PackedScene CannonballScene { get; set; }
[Export] public float ExplosionRadius { get; set; } = 60.0f; // 爆炸范围
public CannonTower()
{
AttackRange = 150.0f;
AttackInterval = 2.0f; // 攻速慢:每 2 秒射一次
Damage = 25; // 伤害高
TowerCost = 100;
}
protected override void PerformAttack()
{
base.PerformAttack();
if (_currentTarget == null || CannonballScene == null) return;
// 生成炮弹
var cannonball = CannonballScene.Instantiate<ProjectileBase>();
cannonball.GlobalPosition = _turret.GlobalPosition;
cannonball.Damage = Damage;
cannonball.Target = _currentTarget;
cannonball.ExplosionRadius = ExplosionRadius;
var container = GetNode<Node2D>("/root/Game/ProjectileContainer");
container.AddChild(cannonball);
}
}GDScript
extends "res://scripts/towers/tower_base.gd"
## 炮塔——发射炮弹,命中后产生范围爆炸。
## 优点:范围伤害,适合对付密集的敌人
## 缺点:攻速慢
@export var cannonball_scene: PackedScene
@export var explosion_radius: float = 60.0 # 爆炸范围
func _init():
attack_range = 150.0
attack_interval = 2.0 # 攻速慢:每 2 秒射一次
damage = 25 # 伤害高
tower_cost = 100
func perform_attack():
super.perform_attack()
if current_target == null or cannonball_scene == null:
return
# 生成炮弹
var cannonball = cannonball_scene.instantiate()
cannonball.global_position = turret.global_position
cannonball.damage = damage
cannonball.target = current_target
cannonball.explosion_radius = explosion_radius
var container = get_node("/root/Game/ProjectileContainer")
container.add_child(cannonball)冰塔——减速效果
冰塔伤害低,但可以减慢敌人的移动速度,让其他塔有更多时间攻击。
C
using Godot;
/// <summary>
/// 冰塔——发射冰弹,减缓敌人移动速度。
/// 优点:减速控制,让其他塔有更多输出时间
/// 缺点:直接伤害很低
/// </summary>
public partial class IceTower : TowerBase
{
[Export] public PackedScene IceProjectileScene { get; set; }
[Export] public float SlowFactor { get; set; } = 0.5f; // 减速比例(0.5 = 减速到50%)
[Export] public float SlowDuration { get; set; } = 2.0f; // 减速持续时间
public IceTower()
{
AttackRange = 130.0f;
AttackInterval = 1.2f;
Damage = 3; // 伤害很低
TowerCost = 75;
}
protected override void PerformAttack()
{
base.PerformAttack();
if (_currentTarget == null || IceProjectileScene == null) return;
var projectile = IceProjectileScene.Instantiate<ProjectileBase>();
projectile.GlobalPosition = _turret.GlobalPosition;
projectile.Damage = Damage;
projectile.Target = _currentTarget;
projectile.SlowFactor = SlowFactor;
projectile.SlowDuration = SlowDuration;
var container = GetNode<Node2D>("/root/Game/ProjectileContainer");
container.AddChild(projectile);
}
}GDScript
extends "res://scripts/towers/tower_base.gd"
## 冰塔——发射冰弹,减缓敌人移动速度。
## 优点:减速控制,让其他塔有更多输出时间
## 缺点:直接伤害很低
@export var ice_projectile_scene: PackedScene
@export var slow_factor: float = 0.5 # 减速比例(0.5 = 减速到50%)
@export var slow_duration: float = 2.0 # 减速持续时间
func _init():
attack_range = 130.0
attack_interval = 1.2
damage = 3 # 伤害很低
tower_cost = 75
func perform_attack():
super.perform_attack()
if current_target == null or ice_projectile_scene == null:
return
var projectile = ice_projectile_scene.instantiate()
projectile.global_position = turret.global_position
projectile.damage = damage
projectile.target = current_target
projectile.slow_factor = slow_factor
projectile.slow_duration = slow_duration
var container = get_node("/root/Game/ProjectileContainer")
container.add_child(projectile)弹丸(子弹)基类
子弹从塔飞向敌人,命中后造成伤害(和可能的效果)。
C
using Godot;
/// <summary>
/// 弹丸基类——从塔飞向敌人的子弹。
/// 追踪型弹丸会自动追踪目标,直到命中或目标死亡。
/// </summary>
public partial class ProjectileBase : Area2D
{
[Export] public float Speed { get; set; } = 300.0f; // 飞行速度
[Export] public int Damage { get; set; } = 10; // 伤害
[Export] public float ExplosionRadius { get; set; } = 0f; // 爆炸范围(0=单体)
[Export] public float SlowFactor { get; set; } = 0f; // 减速比例
[Export] public float SlowDuration { get; set; } = 0f; // 减速持续时间
public Node2D Target { get; set; }
public override void _PhysicsProcess(double delta)
{
// 目标已死亡,销毁自己
if (Target == null || !IsInstanceValid(Target) || Target.IsQueuedForDeletion())
{
QueueFree();
return;
}
// 朝目标飞行
Vector2 direction = GlobalPosition.DirectionTo(Target.GlobalPosition);
Position += direction * Speed * (float)delta;
// 旋转朝向飞行方向
Rotation = direction.Angle();
// 检查是否到达目标附近
if (GlobalPosition.DistanceTo(Target.GlobalPosition) < 10.0f)
{
OnHit();
}
}
/// <summary>命中目标</summary>
protected virtual void OnHit()
{
if (ExplosionRadius > 0f)
{
// 范围伤害:检查爆炸范围内的所有敌人
var spaceState = GetWorld2D().DirectSpaceState;
var query = new PhysicsPointQueryParameters2D
{
Position = GlobalPosition,
Radius = ExplosionRadius,
CollisionMask = 2 // enemy 层
};
var results = spaceState.IntersectPoint(query);
foreach (var result in results)
{
if (result["collider"] is EnemyBase enemy)
enemy.TakeDamage(Damage);
}
}
else if (Target is EnemyBase enemy)
{
// 单体伤害
enemy.TakeDamage(Damage);
}
// 减速效果
if (SlowFactor > 0f && Target is EnemyBase slowTarget)
{
slowTarget.ApplySlow(SlowFactor, SlowDuration);
}
QueueFree(); // 弹丸消失
}
}GDScript
extends Area2D
## 弹丸基类——从塔飞向敌人的子弹。
## 追踪型弹丸会自动追踪目标,直到命中或目标死亡。
@export var speed: float = 300.0 # 飞行速度
@export var damage: int = 10 # 伤害
@export var explosion_radius: float = 0.0 # 爆炸范围(0=单体)
@export var slow_factor: float = 0.0 # 减速比例
@export var slow_duration: float = 0.0 # 减速持续时间
var target: Node2D = null
func _physics_process(delta):
# 目标已死亡,销毁自己
if target == null or not is_instance_valid(target) or target.is_queued_for_deletion():
queue_free()
return
# 朝目标飞行
var direction = global_position.direction_to(target.global_position)
position += direction * speed * delta
# 旋转朝向飞行方向
rotation = direction.angle()
# 检查是否到达目标附近
if global_position.distance_to(target.global_position) < 10.0:
_on_hit()
## 命中目标
func _on_hit():
if explosion_radius > 0.0:
# 范围伤害:检查爆炸范围内的所有敌人
var space_state = get_world_2d().direct_space_state
var query = PhysicsPointQueryParameters2D.new()
query.position = global_position
query.radius = explosion_radius
query.collision_mask = 2 # enemy 层
var results = space_state.intersect_point(query)
for result in results:
if result["collider"] is EnemyBase:
result["collider"].take_damage(damage)
elif target is EnemyBase:
# 单体伤害
target.take_damage(damage)
# 减速效果
if slow_factor > 0.0 and target is EnemyBase:
target.apply_slow(slow_factor, slow_duration)
queue_free() # 弹丸消失放置塔的交互逻辑
玩家选择一个塔类型后,点击地图上的网格位置来放置。
C
using Godot;
/// <summary>
/// 塔放置控制器——处理玩家放置塔的交互。
/// 玩家在面板中选择了塔类型后,鼠标变成"放置模式",
/// 点击地图即可放置。
/// </summary>
public partial class TowerPlacementController : Node2D
{
private GridManager _gridManager;
private PackedScene _selectedTowerScene;
private Sprite2D _preview;
// 当前选中的塔数据
private TowerBase _previewTowerData;
/// <summary>选择要放置的塔类型</summary>
public void SelectTower(PackedScene towerScene)
{
_selectedTowerScene = towerScene;
var tempInstance = towerScene.Instantiate<TowerBase>();
_previewTowerData = tempInstance;
// 检查金币是否足够
if (GameManager.Instance.Gold < _previewTowerData.TowerCost)
{
GD.PrintErr("金币不足!");
return;
}
}
public override void _Ready()
{
_gridManager = GetParent<GridManager>();
}
public override void _UnhandledInput(InputEvent @event)
{
if (_selectedTowerScene == null) return;
// 鼠标左键:放置塔
if (@event is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left)
{
TryPlaceTower();
}
// 鼠标右键或 Esc:取消放置
if (@event is InputEventMouseButton mb2 && mb2.Pressed && mb2.ButtonIndex == MouseButton.Right)
{
CancelPlacement();
}
if (@event is InputEventKey key && key.Pressed && key.Keycode == Key.Escape)
{
CancelPlacement();
}
}
/// <summary>尝试在鼠标位置放置塔</summary>
private void TryPlaceTower()
{
Vector2 mousePos = GetGlobalMousePosition();
Vector2 gridPos = _gridManager.SnapToGrid(mousePos);
// 检查位置是否可用且金币足够
if (!_gridManager.IsCellAvailable(gridPos))
{
GD.Print("该位置已被占用!");
return;
}
if (GameManager.Instance.Gold < _previewTowerData.TowerCost)
{
GD.Print("金币不足!");
return;
}
// 扣除金币
GameManager.Instance.Gold -= _previewTowerData.TowerCost;
// 实例化塔并放置
var tower = _selectedTowerScene.Instantiate<TowerBase>();
tower.GlobalPosition = gridPos;
var container = GetNode<Node2D>("/root/Game/TowerContainer");
container.AddChild(tower);
// 标记网格为已占用
_gridManager.OccupyCell(gridPos, tower);
}
/// <summary>取消放置</summary>
private void CancelPlacement()
{
_selectedTowerScene = null;
_previewTowerData = null;
}
}GDScript
extends Node2D
## 塔放置控制器——处理玩家放置塔的交互。
## 玩家在面板中选择了塔类型后,鼠标变成"放置模式",
## 点击地图即可放置。
var grid_manager: GridManager
var selected_tower_scene: PackedScene = null
var preview_tower_data: TowerBase = null
func _ready():
grid_manager = get_parent()
## 选择要放置的塔类型
func select_tower(tower_scene: PackedScene):
selected_tower_scene = tower_scene
var temp_instance = tower_scene.instantiate()
preview_tower_data = temp_instance
# 检查金币是否足够
if GameManager.instance.gold < preview_tower_data.tower_cost:
push_error("金币不足!")
func _unhandled_input(event):
if selected_tower_scene == null:
return
# 鼠标左键:放置塔
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
_try_place_tower()
# 鼠标右键或 Esc:取消放置
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_RIGHT:
_cancel_placement()
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
_cancel_placement()
## 尝试在鼠标位置放置塔
func _try_place_tower():
var mouse_pos = get_global_mouse_position()
var grid_pos = grid_manager.snap_to_grid(mouse_pos)
# 检查位置是否可用且金币足够
if not grid_manager.is_cell_available(grid_pos):
push_warning("该位置已被占用!")
return
if GameManager.instance.gold < preview_tower_data.tower_cost:
push_warning("金币不足!")
return
# 扣除金币
GameManager.instance.gold -= preview_tower_data.tower_cost
# 实例化塔并放置
var tower = selected_tower_scene.instantiate()
tower.global_position = grid_pos
var container = get_node("/root/Game/TowerContainer")
container.add_child(tower)
# 标记网格为已占用
grid_manager.occupy_cell(grid_pos, tower)
## 取消放置
func _cancel_placement():
selected_tower_scene = null
preview_tower_data = null下一章
防御塔系统能正常工作了,接下来实现敌人波次系统。
