7. 基地防御
2026/4/14大约 9 分钟
7. 坦克大战——基地防御
简介
基地是地图最下方中间的鹰标志。你可以把它想象成足球比赛里的球门——不管你踢得多好,只要球门被攻破了,你就输了。同样,在坦克大战里,不管你打了多少分、消灭了多少敌人,只要基地被敌人的子弹打到了,游戏就直接结束。
基地防御系统包括三个部分:
- 基地本身:被击中就游戏结束
- 基地周围的墙壁:初始是砖墙,可以被打碎
- 道具系统:获取道具来加强防御或攻击
道具类型
道具就像游戏里的"锦囊妙计"——捡到了就能获得临时或永久的能力提升。坦克大战有三种道具:
| 道具 | 外观 | 效果 | 持续时间 |
|---|---|---|---|
| 星星 | 五角星图标 | 玩家子弹升级(1级→2级→3级) | 永久(本关内) |
| 炸弹 | 炸弹图标 | 消灭屏幕上所有敌人 | 瞬间生效 |
| 铲子 | 铲子图标 | 基地周围的砖墙变成钢墙 | 15秒 |
道具掉落规则
当敌人被消灭时,有 25%的概率 在原地掉落一个道具。道具会在原地闪烁一段时间(约10秒),如果玩家不去捡,道具会自动消失。
道具基类
三种道具有共同的行为(都会闪烁、都会在一段时间后消失),所以先写一个基类:
// ItemBase.cs - 道具基类
using Godot;
public partial class ItemBase : Area2D
{
// ========== 属性 ==========
// 道具存在时间(秒),超时自动消失
[Export] public float Lifespan { get; set; } = 10.0f;
// 闪烁开始时间(最后3秒开始闪烁)
[Export] public float BlinkStartTime { get; set; } = 7.0f;
// 道具类型名称
protected string _itemType = "unknown";
// ========== 节点引用 ==========
protected Sprite2D _sprite;
protected Timer _lifespanTimer;
// ========== 生命周期 ==========
public override void _Ready()
{
_sprite = GetNode<Sprite2D>("Sprite2D");
// 创建存活计时器
_lifespanTimer = new Timer { WaitTime = Lifespan, OneShot = true };
_lifespanTimer.Timeout += OnLifespanTimeout;
AddChild(_lifespanTimer);
_lifespanTimer.Start();
// 连接碰撞检测
BodyEntered += OnBodyEntered;
// 开始闪烁计时
var blinkTimer = new Timer { WaitTime = BlinkStartTime, OneShot = true };
blinkTimer.Timeout += StartBlinking;
AddChild(blinkTimer);
blinkTimer.Start();
}
// ========== 碰撞处理 ==========
// 玩家碰到道具时触发
private void OnBodyEntered(Node2D body)
{
if (body is PlayerTank player && player.IsAlive)
{
// 玩家捡起了道具
ApplyEffect(player);
PlayPickupEffect();
QueueFree();
}
}
// ========== 子类必须实现的方法 ==========
// 道具效果(子类重写此方法实现具体效果)
protected virtual void ApplyEffect(PlayerTank player)
{
GD.Print($"玩家获得了 {_itemType} 道具");
}
// ========== 闪烁效果 ==========
// 开始闪烁
private void StartBlinking()
{
var tween = CreateTween();
// 循环闪烁:可见 → 不可见 → 可见 → ...
tween.SetLoops();
tween.TweenProperty(_sprite, "modulate:a", 0.2f, 0.3f);
tween.TweenProperty(_sprite, "modulate:a", 1.0f, 0.3f);
}
// 道具超时消失
private void OnLifespanTimeout()
{
// 消失动画
var tween = CreateTween();
tween.TweenProperty(this, "scale", Vector2.Zero, 0.3f);
tween.TweenCallback(Callable.From(QueueFree));
}
// 捡起道具时的特效
protected void PlayPickupEffect()
{
// 创建一个向上飘动并逐渐消失的文字效果
var label = new Label
{
Text = GetItemName(),
Position = new Vector2(-20, -20),
Theme = ThemeDB.FallbackTheme
};
var tween = CreateTween();
tween.TweenProperty(label, "position:y", -60.0f, 0.8f);
tween.Parallel().TweenProperty(label, "modulate:a", 0.0f, 0.8f);
tween.TweenCallback(Callable.From(() => label.QueueFree()));
AddChild(label);
}
// 获取道具名称(用于显示)
protected virtual string GetItemName()
{
return _itemType;
}
}# item_base.gd - 道具基类
extends Area2D
# ========== 属性 ==========
## 道具存在时间(秒),超时自动消失
@export var lifespan: float = 10.0
## 闪烁开始时间(最后3秒开始闪烁)
@export var blink_start_time: float = 7.0
# 道具类型名称
var item_type: String = "unknown"
# ========== 节点引用 ==========
@onready var sprite: Sprite2D = $Sprite2D
# ========== 生命周期 ==========
func _ready() -> void:
# 创建存活计时器
var lifespan_timer = Timer.new()
lifespan_timer.wait_time = lifespan
lifespan_timer.one_shot = true
lifespan_timer.timeout.connect(on_lifespan_timeout)
add_child(lifespan_timer)
lifespan_timer.start()
# 连接碰撞检测
body_entered.connect(on_body_entered)
# 开始闪烁计时
var blink_timer = Timer.new()
blink_timer.wait_time = blink_start_time
blink_timer.one_shot = true
blink_timer.timeout.connect(start_blinking)
add_child(blink_timer)
blink_timer.start()
# ========== 碰撞处理 ==========
## 玩家碰到道具时触发
func on_body_entered(body: Node2D) -> void:
if body is PlayerTank and body.is_alive:
# 玩家捡起了道具
apply_effect(body)
play_pickup_effect()
queue_free()
# ========== 子类必须实现的方法 ==========
## 道具效果(子类重写此方法实现具体效果)
func apply_effect(player: Node) -> void:
print("玩家获得了 %s 道具" % item_type)
# ========== 闪烁效果 ==========
## 开始闪烁
func start_blinking() -> void:
var tween = create_tween()
# 循环闪烁:可见 → 不可见 → 可见 → ...
tween.set_loops()
tween.tween_property(sprite, "modulate:a", 0.2, 0.3)
tween.tween_property(sprite, "modulate:a", 1.0, 0.3)
## 道具超时消失
func on_lifespan_timeout() -> void:
# 消失动画
var tween = create_tween()
tween.tween_property(self, "scale", Vector2.ZERO, 0.3)
tween.tween_callback(queue_free)
## 捡起道具时的特效
func play_pickup_effect() -> void:
# 创建一个向上飘动并逐渐消失的文字效果
var label = Label.new()
label.text = get_item_name()
label.position = Vector2(-20, -20)
label.theme = ThemeDB.fallback_theme
var tween = create_tween()
tween.tween_property(label, "position:y", -60.0, 0.8)
tween.parallel().tween_property(label, "modulate:a", 0.0, 0.8)
tween.tween_callback(label.queue_free)
add_child(label)
## 获取道具名称(用于显示)
func get_item_name() -> String:
return item_type三种道具实现
星星道具(子弹升级)
// StarItem.cs - 星星道具(子弹升级)
using Godot;
public partial class StarItem : ItemBase
{
public StarItem()
{
_itemType = "星星";
}
protected override void ApplyEffect(PlayerTank player)
{
// 升级子弹
player.UpgradeBullet();
GD.Print($"子弹升级到等级 {player.BulletLevel}");
base.ApplyEffect(player);
}
protected override string GetItemName()
{
return "子弹升级!";
}
}# star_item.gd - 星星道具(子弹升级)
extends "res://scripts/items/item_base.gd"
func _init() -> void:
item_type = "星星"
func apply_effect(player: Node) -> void:
# 升级子弹
player.upgrade_bullet()
print("子弹升级到等级 %d" % player.bullet_level)
super.apply_effect(player)
func get_item_name() -> String:
return "子弹升级!"炸弹道具(清屏)
// BombItem.cs - 炸弹道具(消灭所有敌人)
using Godot;
public partial class BombItem : ItemBase
{
public BombItem()
{
_itemType = "炸弹";
}
protected override void ApplyEffect(PlayerTank player)
{
// 获取所有敌人
var enemiesContainer = GetNode("/root/Game/TanksContainer/Enemies");
var enemies = enemiesContainer.GetChildren();
// 逐个消灭敌人
foreach (var enemy in enemies)
{
if (enemy is EnemyTank enemyTank && enemyTank.IsAlive)
{
// 创建爆炸效果
CreateExplosion(enemyTank.Position);
// 消灭敌人
enemyTank.Destroy();
}
}
// 全屏闪白效果
CreateScreenFlash();
base.ApplyEffect(player);
}
// 在指定位置创建爆炸
private void CreateExplosion(Vector2 pos)
{
var scene = GD.Load<PackedScene>("res://scenes/effects/explosion.tscn");
var explosion = scene.Instantiate<Node2D>();
explosion.Position = pos;
var container = GetNode("/root/Game/EffectsContainer");
container.AddChild(explosion);
}
// 全屏闪白
private void CreateScreenFlash()
{
var flash = new ColorRect
{
Color = new Color(1, 1, 1, 0.8f),
ZIndex = 100
};
// 让 flash 覆盖整个屏幕
flash.SetAnchorsPreset(Control.LayoutPreset.FullRect);
var viewport = GetViewport();
viewport.AddChild(flash);
// 渐渐消失
var tween = flash.CreateTween();
tween.TweenProperty(flash, "color:a", 0.0f, 0.5f);
tween.TweenCallback(Callable.From(flash.QueueFree));
}
protected override string GetItemName()
{
return "全屏炸弹!";
}
}# bomb_item.gd - 炸弹道具(消灭所有敌人)
extends "res://scripts/items/item_base.gd"
func _init() -> void:
item_type = "炸弹"
func apply_effect(player: Node) -> void:
# 获取所有敌人
var enemies_container = get_node("/root/Game/TanksContainer/Enemies")
var enemies = enemies_container.get_children()
# 逐个消灭敌人
for enemy in enemies:
if enemy.is_alive:
# 创建爆炸效果
create_explosion(enemy.position)
# 消灭敌人
enemy.destroy()
# 全屏闪白效果
create_screen_flash()
super.apply_effect(player)
## 在指定位置创建爆炸
func create_explosion(pos: Vector2) -> void:
var scene = load("res://scenes/effects/explosion.tscn")
var explosion = scene.instantiate()
explosion.position = pos
var container = get_node("/root/Game/EffectsContainer")
container.add_child(explosion)
## 全屏闪白
func create_screen_flash() -> void:
var flash = ColorRect.new()
flash.color = Color(1, 1, 1, 0.8)
flash.z_index = 100
# 让 flash 覆盖整个屏幕
flash.set_anchors_preset(Control.PRESET_FULL_RECT)
var viewport = get_viewport()
viewport.add_child(flash)
# 渐渐消失
var tween = flash.create_tween()
tween.tween_property(flash, "color:a", 0.0, 0.5)
tween.tween_callback(flash.queue_free)
func get_item_name() -> String:
return "全屏炸弹!"铲子道具(加固基地)
// ShovelItem.cs - 铲子道具(加固基地)
using Godot;
public partial class ShovelItem : ItemBase
{
public ShovelItem()
{
_itemType = "铲子";
}
// 加固持续时间
private const float FORTIFY_DURATION = 15.0f;
protected override void ApplyEffect(PlayerTank player)
{
// 调用基地管理器来加固
var baseDefense = GetNodeOrNull("/root/Game/BaseDefense");
if (baseDefense != null)
{
baseDefense.Call("fortify_base", FORTIFY_DURATION);
}
base.ApplyEffect(player);
}
protected override string GetItemName()
{
return "基地加固!";
}
}
// BaseDefense.cs - 基地防御管理器
public partial class BaseDefense : Node2D
{
// 基地周围的砖墙位置(相对于基地的位置)
private static readonly Vector2I[] WallOffsets =
{
new(-1, -1), new(0, -1), new(1, -1), // 上方3面墙
new(-1, 0), /*基地*/ new(1, 0), // 两侧
new(-1, 1), new(0, 1), new(1, 1) // 下方3面墙
};
// 是否已加固
private bool _isFortified = false;
// 加固计时器
private Timer _fortifyTimer;
// 原始墙壁引用(用于恢复)
private Node2D[] _originalWalls = new Node2D[8];
// 钢墙场景
private PackedScene _steelWallScene;
public override void _Ready()
{
_steelWallScene = GD.Load<PackedScene>("res://scenes/tiles/steel_wall.tscn");
// 创建加固计时器
_fortifyTimer = new Timer { OneShot = true };
_fortifyTimer.Timeout += OnFortifyExpired;
AddChild(_fortifyTimer);
}
// 加固基地
public void FortifyBase(float duration)
{
if (_isFortified) return;
_isFortified = true;
GD.Print($"基地已加固,持续 {duration} 秒");
var baseNode = GetNode<Node2D>("../Base");
var gridPos = baseNode.Position;
// 把基地周围的砖墙替换成钢墙
for (int i = 0; i < WallOffsets.Length; i++)
{
var wallPos = gridPos + WallOffsets[i] * 48;
// 找到这个位置的砖墙
var walls = GetParent().GetChildren();
foreach (var wall in walls)
{
if (wall is StaticBody2D body && wall.IsInGroup("brick_wall"))
{
if (Mathf.Abs(body.Position.X - wallPos.X) < 10 &&
Mathf.Abs(body.Position.Y - wallPos.Y) < 10)
{
// 保存原始墙壁引用
_originalWalls[i] = wall;
// 隐藏原始砖墙
wall.Visible = false;
// 创建钢墙放在相同位置
var steelWall = _steelWallScene.Instantiate<StaticBody2D>();
steelWall.Position = wallPos;
GetParent().AddChild(steelWall);
break;
}
}
}
}
// 开始倒计时
_fortifyTimer.WaitTime = duration;
_fortifyTimer.Start();
}
// 加固到期
private void OnFortifyExpired()
{
if (!_isFortified) return;
_isFortified = false;
GD.Print("基地加固已到期");
// 删除所有钢墙,恢复砖墙
var walls = GetParent().GetChildren();
foreach (var wall in walls)
{
if (wall is StaticBody2D body && wall.IsInGroup("steel_wall"))
{
wall.QueueFree();
}
}
// 恢复原始砖墙
foreach (var wall in _originalWalls)
{
if (wall != null)
{
wall.Visible = true;
}
}
}
}# shovel_item.gd - 铲子道具(加固基地)
extends "res://scripts/items/item_base.gd"
func _init() -> void:
item_type = "铲子"
# 加固持续时间
const FORTIFY_DURATION: float = 15.0
func apply_effect(player: Node) -> void:
# 调用基地管理器来加固
var base_defense = get_node_or_null("/root/Game/BaseDefense")
if base_defense != null:
base_defense.fortify_base(FORTIFY_DURATION)
super.apply_effect(player)
func get_item_name() -> String:
return "基地加固!"
# base_defense.gd - 基地防御管理器
extends Node2D
# 基地周围的砖墙位置(相对于基地的位置)
var wall_offsets: Array[Vector2i] = [
Vector2i(-1, -1), Vector2i(0, -1), Vector2i(1, -1),
Vector2i(-1, 0), Vector2i(1, 0),
Vector2i(-1, 1), Vector2i(0, 1), Vector2i(1, 1)
]
# 是否已加固
var is_fortified: bool = false
# 加固计时器
var fortify_timer: Timer
# 原始墙壁引用(用于恢复)
var original_walls: Array[Node2D] = []
# 钢墙场景
var steel_wall_scene: PackedScene
func _ready() -> void:
steel_wall_scene = load("res://scenes/tiles/steel_wall.tscn")
# 创建加固计时器
fortify_timer = Timer.new()
fortify_timer.one_shot = true
fortify_timer.timeout.connect(on_fortify_expired)
add_child(fortify_timer)
## 加固基地
func fortify_base(duration: float) -> void:
if is_fortified:
return
is_fortified = true
print("基地已加固,持续 %d 秒" % duration)
var base_node = get_node("../Base")
var grid_pos = base_node.position
# 把基地周围的砖墙替换成钢墙
for i in range(wall_offsets.size()):
var wall_pos = grid_pos + Vector2(wall_offsets[i].x * 48, wall_offsets[i].y * 48)
# 找到这个位置的砖墙并替换
var parent = get_parent()
for wall in parent.get_children():
if wall is StaticBody2D and wall.is_in_group("brick_wall"):
if absf(wall.position.x - wall_pos.x) < 10 and absf(wall.position.y - wall_pos.y) < 10:
# 保存原始墙壁引用
if original_walls.size() <= i:
original_walls.append(wall)
# 隐藏原始砖墙
wall.visible = false
# 创建钢墙放在相同位置
var steel_wall = steel_wall_scene.instantiate()
steel_wall.position = wall_pos
parent.add_child(steel_wall)
break
# 开始倒计时
fortify_timer.wait_time = duration
fortify_timer.start()
## 加固到期
func on_fortify_expired() -> void:
if not is_fortified:
return
is_fortified = false
print("基地加固已到期")
# 删除所有临时钢墙,恢复砖墙
var parent = get_parent()
for wall in parent.get_children():
if wall is StaticBody2D and wall.is_in_group("steel_wall"):
wall.queue_free()
# 恢复原始砖墙
for wall in original_walls:
if wall != null:
wall.visible = true基地保护总结
整个基地防御系统的工作流程:
游戏开始
↓
基地被砖墙包围(可被破坏)
↓
敌人射击 → 砖墙被破坏 → 基地暴露
↓
玩家获取铲子道具 → 砖墙变钢墙(15秒)→ 期间基地无敌
↓
15秒到期 → 钢墙恢复为砖墙 → 基地再次面临威胁
↓
敌人子弹打到基地 → 游戏结束| 防御手段 | 效果 | 限制 |
|---|---|---|
| 初始砖墙 | 挡住普通子弹 | 可被破坏 |
| 铲子道具 | 把砖墙变钢墙 | 只持续15秒 |
| 星星道具 | 升级子弹,更有效消灭敌人 | 不直接保护基地 |
| 炸弹道具 | 一次清屏,减少威胁 | 不持续 |
| 消灭敌人 | 减少攻击源 | 新敌人会继续生成 |
下一章预告
基地防御和道具系统完成了!下一章我们将实现游戏界面(UI)——包括HUD(分数、生命、关卡信息)、关卡选择界面和地图编辑器的UI。
