5. 升级与出售
2026/4/14大约 11 分钟
5. 保卫萝卜——升级与出售
为什么要升级和出售?
想象你开了一家小店:
- 升级就像给店面装修、扩充——花更多的钱让小店变得更强(攻击更高、射程更远)
- 出售就像把不想要的家具二手卖掉——虽然不能全额退款,但能回收一部分钱来买更好的东西
在塔防游戏中,这两个操作让玩家的策略选择更加丰富:
| 操作 | 好处 | 代价 |
|---|---|---|
| 升级 | 强化已有塔,节省空间 | 需要额外金币 |
| 出售 | 回收金币,腾出位置 | 只能返还 60% |
升级系统设计
升级规则
| 规则 | 说明 |
|---|---|
| 等级上限 | 最多升到 3 级 |
| 费用递增 | 越高级越贵(升2级便宜,升3级贵) |
| 效果递增 | 攻击力、射程随等级提升 |
| 视觉变化 | 每次升级外观有变化,让玩家一眼看出等级 |
升级费用和效果表
以瓶子塔为例:
| 等级 | 攻击力 | 射程 | 升级费用 | 累计投入 |
|---|---|---|---|---|
| 1 级(初始) | 15 | 150 | 0 | 100 |
| 2 级 | 20 | 170 | 80 | 180 |
| 3 级 | 30 | 200 | 150 | 330 |
设计思路:3 级瓶子塔(攻击 30)比买两个 1 级瓶子塔(攻击 15x2=30)更强,因为还额外增加了射程。但价格也更贵(330 vs 200),这就需要玩家做出选择。
升级管理器
升级和出售操作需要一个"交互控制器"来管理——当玩家点击一个已放置的塔时,显示升级/出售选项。
C
using Godot;
/// <summary>
/// 塔交互控制器 —— 管理塔的选中、升级、出售交互
/// 就像"塔的服务台",玩家点击塔后弹出的操作面板
/// </summary>
public partial class TowerInteractionController : Node
{
// ===== 信号 =====
/// <summary>塔被选中时发送</summary>
[Signal]
public delegate void TowerSelectedEventHandler(Tower tower);
/// <summary>取消选中时发送</summary>
[Signal]
public delegate void TowerDeselectedEventHandler();
// ===== 状态 =====
private Tower _selectedTower;
/// <summary>当前选中的塔</summary>
public Tower SelectedTower => _selectedTower;
/// <summary>是否有塔被选中</summary>
public bool HasSelection => _selectedTower != null;
public override void _Ready()
{
// 监听输入事件
// 注意:实际的输入处理在 _Input 中
}
public override void _Input(InputEvent @event)
{
// 鼠标右键或 Escape 取消选中
if (@event is InputEventMouseButton mb
&& mb.ButtonIndex == MouseButton.Right
&& mb.Pressed)
{
DeselectTower();
}
if (@event is InputEventKey key
&& key.Keycode == Key.Escape
&& key.Pressed)
{
DeselectTower();
}
}
/// <summary>
/// 选中一个塔
/// </summary>
public void SelectTower(Tower tower)
{
// 先取消之前的选中
if (_selectedTower != null && _selectedTower != tower)
{
_selectedTower.SetSelected(false);
}
_selectedTower = tower;
tower.SetSelected(true);
EmitSignal(SignalName.TowerSelected, tower);
}
/// <summary>
/// 取消选中
/// </summary>
public void DeselectTower()
{
if (_selectedTower != null)
{
_selectedTower.SetSelected(false);
_selectedTower = null;
}
EmitSignal(SignalName.TowerDeselected);
}
/// <summary>
/// 升级选中的塔
/// </summary>
/// <returns>是否升级成功</returns>
public bool UpgradeSelectedTower()
{
if (_selectedTower == null) return false;
// 获取升级费用
int cost = GetUpgradeCost(_selectedTower);
if (cost <= 0)
{
GD.Print("塔已满级,无法升级");
return false;
}
return _selectedTower.Upgrade(cost);
}
/// <summary>
/// 出售选中的塔
/// </summary>
public void SellSelectedTower()
{
if (_selectedTower == null) return;
var tower = _selectedTower;
DeselectTower();
tower.Sell();
}
/// <summary>
/// 获取升级费用
/// </summary>
public int GetUpgradeCost(Tower tower)
{
if (tower.CurrentLevel >= tower.MaxLevel)
return 0; // 已满级
// 从塔数据中获取对应等级的升级费用
int levelIndex = tower.CurrentLevel - 1;
// 简化处理:费用随等级递增
return tower.PurchaseCost * (tower.CurrentLevel + 1) / 2;
}
/// <summary>
/// 获取出售返还金额
/// </summary>
public int GetSellRefund(Tower tower)
{
return Mathf.FloorToInt(
tower.TotalInvested * GameConstants.SellRefundRate);
}
}GDScript
extends Node
class_name TowerInteractionController
## 塔交互控制器 —— 管理塔的选中、升级、出售交互
## 就像"塔的服务台",玩家点击塔后弹出的操作面板
# ===== 信号 =====
## 塔被选中时发送
signal tower_selected(tower: Tower)
## 取消选中时发送
signal tower_deselected()
# ===== 状态 =====
var _selected_tower: Tower = null
## 当前选中的塔
var selected_tower: Tower:
get: return _selected_tower
## 是否有塔被选中
var has_selection: bool:
get: return _selected_tower != null
func _input(event: InputEvent) -> void:
# 鼠标右键或 Escape 取消选中
if event is InputEventMouseButton mb:
if mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed:
_deselect_tower()
if event is InputEventKey key:
if key.keycode == KEY_ESCAPE and key.pressed:
_deselect_tower()
## 选中一个塔
func select_tower(tower: Tower) -> void:
# 先取消之前的选中
if _selected_tower != null and _selected_tower != tower:
_selected_tower.set_selected(false)
_selected_tower = tower
tower.set_selected(true)
tower_selected.emit(tower)
## 取消选中
func _deselect_tower() -> void:
if _selected_tower != null:
_selected_tower.set_selected(false)
_selected_tower = null
tower_deselected.emit()
## 升级选中的塔
func upgrade_selected_tower() -> bool:
if _selected_tower == null:
return false
var cost: int = _get_upgrade_cost(_selected_tower)
if cost <= 0:
print("塔已满级,无法升级")
return false
return _selected_tower.upgrade(cost)
## 出售选中的塔
func sell_selected_tower() -> void:
if _selected_tower == null:
return
var tower: Tower = _selected_tower
_deselect_tower()
tower.sell()
## 获取升级费用
func _get_upgrade_cost(tower: Tower) -> int:
if tower.current_level >= tower.max_level:
return 0 # 已满级
# 费用随等级递增
return tower.purchase_cost * (tower.current_level + 1) / 2
## 获取出售返还金额
func _get_sell_refund(tower: Tower) -> int:
return floori(tower.total_invested * GameConstants.SELL_REFUND_RATE)塔的点击检测
玩家需要能够通过点击来选中已放置的塔。我们给每个塔添加一个可点击区域。
C
using Godot;
/// <summary>
/// 塔的可点击区域组件
/// 挂载在每个塔场景上,处理鼠标点击选中逻辑
/// </summary>
public partial class TowerClickArea : Area2D
{
/// <summary>所属的塔</summary>
private Tower _parentTower;
public override void _Ready()
{
_parentTower = GetParent<Tower>();
// 连接鼠标点击信号
InputEvent += OnInputEvent;
// 设置碰撞层:只在第2层(Tower层)检测
CollisionLayer = 0;
CollisionMask = 0;
}
/// <summary>
/// 鼠标点击事件处理
/// </summary>
private void OnInputEvent(Node viewport, InputEvent @event, int shapeIdx)
{
if (@event is InputEventMouseButton mb
&& mb.ButtonIndex == MouseButton.Left
&& mb.Pressed)
{
// 通知交互控制器选中此塔
var controller = GetTree()
.Root.FindChild("TowerInteractionController", true, false);
if (controller is TowerInteractionController tic)
{
tic.SelectTower(_parentTower);
}
}
}
/// <summary>
/// 鼠标悬停效果
/// </summary>
public override void _Process(double delta)
{
// 鼠标悬停时改变光标
var mousePos = GetGlobalMousePosition();
bool isHovering = GetNode<CollisionShape2D>("CollisionShape2D")
.Shape.GetRect().HasPoint(ToLocal(mousePos));
if (isHovering)
{
Input.DefaultCursorShape = Input.CursorShape.PointingHand;
}
}
}GDScript
extends Area2D
class_name TowerClickArea
## 塔的可点击区域组件
## 挂载在每个塔场景上,处理鼠标点击选中逻辑
## 所属的塔
var _parent_tower: Tower
func _ready() -> void:
_parent_tower = get_parent() as Tower
# 连接鼠标点击信号
input_event.connect(_on_input_event)
# 设置碰撞层
collision_layer = 0
collision_mask = 0
## 鼠标点击事件处理
func _on_input_event(_viewport: Node, event: InputEvent, _shape_idx: int) -> void:
if event is InputEventMouseButton mb:
if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
# 通知交互控制器选中此塔
var controller = get_tree().root.find_child(
"TowerInteractionController", true, false
)
if controller is TowerInteractionController tic:
tic.select_tower(_parent_tower)升级动画效果
升级时应该有明显的视觉反馈,让玩家感受到"变强了"。
C
using Godot;
/// <summary>
/// 塔升级动画控制器
/// 提供丰富的升级视觉反馈
/// </summary>
public partial class TowerUpgradeEffect : Node2D
{
/// <summary>
/// 播放升级动画
/// </summary>
public void PlayUpgradeAnimation(
Sprite2D towerSprite,
int newLevel)
{
// 1. 放大再缩回
var tween = CreateTween();
tween.TweenProperty(towerSprite, "scale",
new Vector2(1.4f, 1.4f), 0.15f)
.SetTrans(Tween.TransitionType.Back);
tween.TweenProperty(towerSprite, "scale",
new Vector2(1.0f, 1.0f), 0.15f);
// 2. 金色闪光
var flashOverlay = new ColorRect
{
Color = new Color(1, 0.84f, 0, 0.5f) // 金色半透明
};
// 3. 上升的金色粒子
var particles = new GpuParticles2D
{
Amount = 15,
OneShot = true,
Emitting = true,
Lifetime = 1.0f,
Direction = new Vector2(0, -1), // 向上飘
Spread = 45.0f,
Gravity = new Vector2(0, -20),
};
AddChild(particles);
// 4. 显示升级文字
ShowLevelUpText(towerSprite, newLevel);
}
/// <summary>
/// 显示 "Lv.2" 这样的升级文字
/// </summary>
private void ShowLevelUpText(Sprite2D parent, int level)
{
var label = new Label
{
Text = $"Lv.{level}",
HorizontalAlignment = HorizontalAlignment.Center
};
label.Position = new Vector2(-20, -40);
label.AddThemeFontSizeOverride(
"font_size", 18);
label.AddThemeColorOverride(
"font_color", Colors.Gold);
parent.AddChild(label);
// 文字向上飘并淡出
var tween = CreateTween();
tween.TweenProperty(label, "position",
new Vector2(-20, -70), 1.0f);
tween.Parallel().TweenProperty(label, "modulate",
new Color(1, 0.84f, 0, 0), 1.0f);
tween.TweenCallback(Callable.From(() => label.QueueFree()));
}
}GDScript
extends Node2D
class_name TowerUpgradeEffect
## 塔升级动画控制器
## 提供丰富的升级视觉反馈
## 播放升级动画
func play_upgrade_animation(tower_sprite: Sprite2D, new_level: int) -> void:
# 1. 放大再缩回
var tween = create_tween()
tween.tween_property(
tower_sprite, "scale",
Vector2(1.4, 1.4), 0.15
).set_trans(Tween.TRANS_BACK)
tween.tween_property(
tower_sprite, "scale",
Vector2(1.0, 1.0), 0.15
)
# 2. 金色闪光
var flash_overlay = ColorRect.new()
flash_overlay.color = Color(1, 0.84, 0, 0.5)
tower_sprite.add_child(flash_overlay)
var flash_tween = create_tween()
flash_tween.tween_property(flash_overlay, "color", Color(1, 0.84, 0, 0), 0.3)
flash_tween.tween_callback(flash_overlay.queue_free)
# 3. 显示升级文字
_show_level_up_text(tower_sprite, new_level)
## 显示升级文字
func _show_level_up_text(parent: Sprite2D, level: int) -> void:
var label = Label.new()
label.text = "Lv.%d" % level
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.position = Vector2(-20, -40)
label.add_theme_font_size_override("font_size", 18)
label.add_theme_color_override("font_color", Color.GOLD)
parent.add_child(label)
# 文字向上飘并淡出
var tween = create_tween()
tween.tween_property(label, "position", Vector2(-20, -70), 1.0)
tween.parallel().tween_property(label, "modulate", Color(1, 0.84, 0, 0), 1.0)
tween.tween_callback(label.queue_free)建造系统
升级和出售是针对已放置的塔的操作,但"放置"本身也需要一个系统来管理。
C
using Godot;
/// <summary>
/// 建造控制器 —— 管理放置新塔的逻辑
/// 就像"施工队队长",负责把塔放到正确的位置
/// </summary>
public partial class BuildController : Node
{
[Signal]
public delegate void BuildModeChangedEventHandler(
bool isBuilding, TowerData towerData);
[Signal]
public delegate void TowerBuiltEventHandler(Tower tower);
/// <summary>塔容器</summary>
[Export] public Node2D TowersContainer { get; set; }
/// <summary>可建造区域地图</summary>
[Export] public TileMapLayer BuildableMap { get; set; }
/// <summary>所有塔类型的数据</summary>
[Export] public TowerData[] AvailableTowers { get; set; }
private bool _isBuildingMode = false;
private TowerData _selectedTowerData;
private Node2D _previewTower; // 放置预览(半透明)
public bool IsBuildingMode => _isBuildingMode;
/// <summary>
/// 进入建造模式
/// </summary>
public void EnterBuildMode(TowerData data)
{
// 检查金币是否足够
if (GameManager.Instance.Gold < data.Cost)
{
GD.Print($"金币不足!需要 {data.Cost},当前 {GameManager.Instance.Gold}");
return;
}
_isBuildingMode = true;
_selectedTowerData = data;
// 创建预览塔(半透明显示)
CreatePreview(data);
EmitSignal(SignalName.BuildModeChanged, true, data);
}
/// <summary>
/// 退出建造模式
/// </summary>
public void ExitBuildMode()
{
_isBuildingMode = false;
_selectedTowerData = null;
// 移除预览
if (_previewTower != null)
{
_previewTower.QueueFree();
_previewTower = null;
}
EmitSignal(SignalName.BuildModeChanged, false, null);
}
public override void _Process(double delta)
{
if (!_isBuildingMode || _previewTower == null) return;
// 预览塔跟随鼠标
_previewTower.GlobalPosition = SnapToGrid(GetGlobalMousePosition());
}
public override void _Input(InputEvent @event)
{
if (!_isBuildingMode) return;
// 左键放置
if (@event is InputEventMouseButton mb
&& mb.ButtonIndex == MouseButton.Left
&& mb.Pressed)
{
TryPlaceTower();
}
// 右键取消
if (@event is InputEventMouseButton mb
&& mb.ButtonIndex == MouseButton.Right
&& mb.Pressed)
{
ExitBuildMode();
}
if (@event is InputEventKey key
&& key.Keycode == Key.Escape
&& key.Pressed)
{
ExitBuildMode();
}
}
/// <summary>
/// 尝试在鼠标位置放置塔
/// </summary>
private void TryPlaceTower()
{
Vector2 gridPos = SnapToGrid(GetGlobalMousePosition());
// 检查该位置是否可以建造
if (!CanBuildAt(gridPos))
{
GD.Print("该位置无法建造!");
return;
}
// 扣除金币
if (!GameManager.Instance.SpendGold(_selectedTowerData.Cost))
{
GD.Print("金币不足!");
ExitBuildMode();
return;
}
// 创建塔
var tower = _selectedTowerData.TowerScene
.Instantiate<Tower>();
tower.GlobalPosition = gridPos;
TowersContainer.AddChild(tower);
// 标记该格子为已占用
MarkGridCell(gridPos, true);
// 添加到 monsters 组(用于目标检测)
// 这里用 towers 组
tower.AddToGroup("towers");
EmitSignal(SignalName.TowerBuilt, tower);
// 放置成功后退出建造模式
// (如果玩家想连续放置,可以不退出)
ExitBuildMode();
}
/// <summary>
/// 将位置对齐到网格
/// </summary>
private Vector2 SnapToGrid(Vector2 position)
{
float halfGrid = GameConstants.GridSize / 2.0f;
return new Vector2(
Mathf.Round(position.X / GameConstants.GridSize) * GameConstants.GridSize + halfGrid,
Mathf.Round(position.Y / GameConstants.GridSize) * GameConstants.GridSize + halfGrid
);
}
/// <summary>
/// 检查某位置是否可以建造
/// </summary>
private bool CanBuildAt(Vector2 position)
{
// 检查是否已有塔
var towers = GetTree().GetNodesInGroup("towers");
foreach (var node in towers)
{
if (node is Tower t)
{
float dist = position.DistanceTo(t.GlobalPosition);
if (dist < GameConstants.GridSize * 0.8f)
return false;
}
}
return true;
}
/// <summary>
/// 创建放置预览
/// </summary>
private void CreatePreview(TowerData data)
{
_previewTower = data.TowerScene.Instantiate<Node2D>();
_previewTower.Modulate = new Color(1, 1, 1, 0.5f); // 半透明
GetTree().Root.AddChild(_previewTower);
}
/// <summary>
/// 标记网格格子为已占用/已清空
/// </summary>
private void MarkGridCell(Vector2 position, bool occupied)
{
// 通过 TileMap 来标记
// 简化处理:实际项目中可以用一个二维数组
}
}GDScript
extends Node
class_name BuildController
## 建造控制器 —— 管理放置新塔的逻辑
## 就像"施工队队长",负责把塔放到正确的位置
signal build_mode_changed(is_building: bool, tower_data: TowerData)
signal tower_built(tower: Tower)
## 塔容器
@export var towers_container: Node2D
## 可建造区域地图
@export var buildable_map: TileMapLayer
## 所有塔类型的数据
@export var available_towers: Array[TowerData] = []
var _is_building_mode: bool = false
var _selected_tower_data: TowerData = null
var _preview_tower: Node2D = null # 放置预览
var is_building_mode: bool:
get: return _is_building_mode
## 进入建造模式
func enter_build_mode(data: TowerData) -> void:
if GameManager.gold < data.cost:
push_error("金币不足!需要 %d,当前 %d" % [data.cost, GameManager.gold])
return
_is_building_mode = true
_selected_tower_data = data
# 创建预览塔
_create_preview(data)
build_mode_changed.emit(true, data)
## 退出建造模式
func exit_build_mode() -> void:
_is_building_mode = false
_selected_tower_data = null
if _preview_tower:
_preview_tower.queue_free()
_preview_tower = null
build_mode_changed.emit(false, null)
func _process(_delta: float) -> void:
if not _is_building_mode or not _preview_tower:
return
_preview_tower.global_position = _snap_to_grid(get_global_mouse_position())
func _input(event: InputEvent) -> void:
if not _is_building_mode:
return
if event is InputEventMouseButton mb:
if mb.button_index == MOUSE_BUTTON_LEFT and mb.pressed:
_try_place_tower()
if mb.button_index == MOUSE_BUTTON_RIGHT and mb.pressed:
exit_build_mode()
if event is InputEventKey key:
if key.keycode == KEY_ESCAPE and key.pressed:
exit_build_mode()
## 尝试放置塔
func _try_place_tower() -> void:
var grid_pos: Vector2 = _snap_to_grid(get_global_mouse_position())
if not _can_build_at(grid_pos):
push_warning("该位置无法建造!")
return
if not GameManager.spend_gold(_selected_tower_data.cost):
push_error("金币不足!")
exit_build_mode()
return
var tower: Tower = _selected_tower_data.tower_scene.instantiate()
tower.global_position = grid_pos
towers_container.add_child(tower)
tower.add_to_group("towers")
tower_built.emit(tower)
exit_build_mode()
## 对齐到网格
func _snap_to_grid(position: Vector2) -> Vector2:
var half_grid: float = GameConstants.GRID_SIZE / 2.0
return Vector2(
roundf(position.x / GameConstants.GRID_SIZE) * GameConstants.GRID_SIZE + half_grid,
roundf(position.y / GameConstants.GRID_SIZE) * GameConstants.GRID_SIZE + half_grid
)
## 检查是否可以建造
func _can_build_at(position: Vector2) -> bool:
var towers = get_tree().get_nodes_in_group("towers")
for node in towers:
if node is Tower t:
var dist: float = position.distance_to(t.global_position)
if dist < GameConstants.GRID_SIZE * 0.8:
return false
return true
## 创建放置预览
func _create_preview(data: TowerData) -> void:
_preview_tower = data.tower_scene.instantiate()
_preview_tower.modulate = Color(1, 1, 1, 0.5) # 半透明
get_tree().root.add_child(_preview_tower)出售确认
出售是一个不可逆的操作,所以最好加一个确认步骤,防止玩家误操作。
C
/// <summary>
/// 出售确认弹窗
/// 显示返还金额,让玩家确认后再出售
/// </summary>
public partial class SellConfirmDialog : AcceptDialog
{
private int _refundAmount;
private Tower _targetTower;
public void ShowConfirm(Tower tower, int refund)
{
_targetTower = tower;
_refundAmount = refund;
DialogText = $"确定要出售此塔吗?\n将返还 {_refundAmount} 金币";
Title = "出售确认";
// 连接确认按钮
Confirmed += OnConfirmed;
PopupCentered();
}
private void OnConfirmed()
{
if (_targetTower != null && IsInstanceValid(_targetTower))
{
_targetTower.Sell();
}
Confirmed -= OnConfirmed;
}
}GDScript
extends AcceptDialog
class_name SellConfirmDialog
## 出售确认弹窗
## 显示返还金额,让玩家确认后再出售
var _refund_amount: int = 0
var _target_tower: Tower = null
func show_confirm(tower: Tower, refund: int) -> void:
_target_tower = tower
_refund_amount = refund
dialog_text = "确定要出售此塔吗?\n将返还 %d 金币" % refund
title = "出售确认"
confirmed.connect(_on_confirmed, Object.CONNECT_ONE_SHOT)
popup_centered()
func _on_confirmed() -> void:
if _target_tower and is_instance_valid(_target_tower):
_target_tower.sell()本节小结
| 功能 | 说明 |
|---|---|
| 升级 | 提升攻击力和射程,最多3级,费用递增 |
| 出售 | 返还总投入的60%,不可逆操作 |
| 选中交互 | 点击塔显示攻击范围和操作选项 |
| 建造模式 | 选择塔类型后鼠标跟随预览,点击放置 |
| 升级动画 | 放大缩回、金色闪光、等级文字上飘 |
| 出售确认 | 弹窗确认,防止误操作 |
升级与出售系统让游戏有了更丰富的策略深度——玩家不仅要想"放什么塔",还要想"该升级还是该卖掉买新的"。下一节我们将实现波次系统,让怪物一波一波地出现。
