7. 特殊机制
2026/4/14大约 10 分钟
7. 保卫萝卜——特殊机制
为什么需要特殊机制?
如果只有"放塔打怪"这一种玩法,游戏很快就会变得无聊。特殊机制就像是"调味料"——基础玩法是"白米饭",特殊机制是各种"菜",搭配起来才好吃。
本节要实现的特殊机制:
| 机制 | 比喻 | 作用 |
|---|---|---|
| 可清除障碍物 | 路边的"违建" | 打掉后获得额外建塔位置 |
| 冰冻减速 | 结冰路面 | 让怪物走得非常慢 |
| 持续毒伤 | 中毒状态 | 怪物每秒持续掉血 |
| 范围伤害 | 炸弹 | 一发攻击打一片 |
| 塔组合效果 | 团队配合 | 特定塔靠近时互相增强 |
可清除障碍物系统
在保卫萝卜中,地图上有些"障碍物"(比如石头、树桩),它们占据了一个建塔位置。玩家花费一定金币可以清除它们,从而获得更多的建塔空位。
障碍物数据
C
using Godot;
/// <summary>
/// 障碍物数据 —— 路边"违建"的属性
/// </summary>
[GlobalClass]
public partial class ObstacleData : Resource
{
/// <summary>障碍物名称</summary>
[Export] public string ObstacleName { get; set; } = "石头";
/// <summary>清除费用</summary>
[Export] public int ClearCost { get; set; } = 50;
/// <summary>障碍物生命值(有些需要打掉)</summary>
[Export] public int Hp { get; set; } = 0;
/// <summary>障碍物场景</summary>
[Export] public PackedScene Scene { get; set; }
/// <summary>描述</summary>
[Export] public string Description { get; set; } = "可以清除获得建塔位置";
}GDScript
class_name ObstacleData
extends Resource
## 障碍物数据 —— 路边"违建"的属性
## 障碍物名称
@export var obstacle_name: String = "石头"
## 清除费用
@export var clear_cost: int = 50
## 障碍物生命值
@export var hp: int = 0
## 障碍物场景
@export var scene: PackedScene = null
## 描述
@export var description: String = "可以清除获得建塔位置"障碍物脚本
C
using Godot;
/// <summary>
/// 障碍物 —— 占据建塔位置的可清除物体
/// 有些障碍物需要花金币直接清除
/// 有些障碍物需要防御塔打掉(有生命值)
/// </summary>
public partial class Obstacle : Node2D
{
[Signal]
public delegate void ObstacleClearedEventHandler(
Vector2 position);
[Export] public int ClearCost { get; set; } = 50;
[Export] public int MaxHp { get; set; } = 0; // 0 表示直接清除
private int _currentHp;
private Sprite2D _sprite;
private ProgressBar _healthBar;
public bool NeedsDestroying => MaxHp > 0;
public override void _Ready()
{
_sprite = GetNode<Sprite2D>("Sprite2D");
_currentHp = MaxHp;
if (MaxHp > 0)
{
_healthBar = GetNode<ProgressBar>("HealthBar");
_healthBar.Visible = false;
}
// 添加到碰撞层,使防御塔可以打到它
var area = GetNode<Area2D>("Area2D");
area.CollisionLayer = 1 << (GameConstants.LayerObstacle - 1);
}
/// <summary>
/// 花金币清除(直接清除)
/// </summary>
public bool ClearWithGold()
{
if (NeedsDestroying) return false;
if (!GameManager.Instance.SpendGold(ClearCost)) return false;
PlayClearAnimation();
return true;
}
/// <summary>
/// 受到伤害(被防御塔打)
/// </summary>
public void TakeDamage(int damage)
{
if (!NeedsDestroying) return;
_currentHp -= damage;
if (_healthBar != null)
{
_healthBar.Visible = true;
_healthBar.Value = (float)_currentHp / MaxHp * 100;
}
if (_currentHp <= 0)
{
PlayClearAnimation();
}
}
private async void PlayClearAnimation()
{
if (_sprite != null)
{
var tween = CreateTween();
tween.TweenProperty(_sprite, "scale",
Vector2.Zero, 0.3f);
tween.Parallel().TweenProperty(_sprite, "modulate",
new Color(1, 1, 1, 0), 0.3f);
}
await ToSignal(GetTree().CreateTimer(0.3f),
Timer.SignalName.Timeout);
EmitSignal(SignalName.ObstacleCleared, GlobalPosition);
QueueFree();
}
}GDScript
extends Node2D
class_name Obstacle
## 障碍物 —— 占据建塔位置的可清除物体
signal obstacle_cleared(position: Vector2)
## 清除费用
@export var clear_cost: int = 50
## 最大生命值(0 表示直接花钱清除)
@export var max_hp: int = 0
var _current_hp: int = 0
var _needs_destroying: bool = false
@onready var _sprite: Sprite2D = $Sprite2D
@onready var _health_bar: ProgressBar = $HealthBar
func _ready() -> void:
_current_hp = max_hp
_needs_destroying = max_hp > 0
if max_hp > 0 and _health_bar:
_health_bar.visible = false
## 花金币清除
func clear_with_gold() -> bool:
if _needs_destroying:
return false
if not GameManager.spend_gold(clear_cost):
return false
_play_clear_animation()
return true
## 受到伤害
func take_damage(damage: int) -> void:
if not _needs_destroying:
return
_current_hp -= damage
if _health_bar:
_health_bar.visible = true
_health_bar.value = float(_current_hp) / float(max_hp) * 100
if _current_hp <= 0:
_play_clear_animation()
func _play_clear_animation() -> void:
if _sprite:
var tween = create_tween()
tween.tween_property(_sprite, "scale", Vector2.ZERO, 0.3)
tween.parallel().tween_property(_sprite, "modulate", Color(1, 1, 1, 0), 0.3)
await get_tree().create_timer(0.3).timeout
obstacle_cleared.emit(global_position)
queue_free()效果系统(状态效果)
怪物可以被施加各种"状态效果"(也叫 debuff),就像人感冒了会有流鼻涕、发烧等不同症状一样。
效果类型定义
C
/// <summary>
/// 效果类型枚举
/// </summary>
public enum EffectType
{
/// <summary>冰冻减速</summary>
Freeze,
/// <summary>持续毒伤</summary>
Poison,
/// <summary>眩晕(短暂停住)</summary>
Stun,
/// <summary>易伤(受到的伤害增加)</summary>
Vulnerable
}
/// <summary>
/// 状态效果 —— 施加在怪物身上的"负面状态"
/// 每个效果有类型、持续时间和强度
/// </summary>
public class StatusEffect
{
public EffectType Type { get; }
public float Duration { get; private set; }
public float Intensity { get; }
public float TickInterval { get; }
public int DamagePerTick { get; }
private float _elapsed;
private float _tickTimer;
/// <summary>效果是否过期</summary>
public bool IsExpired => _elapsed >= Duration;
public StatusEffect(
EffectType type,
float duration,
float intensity = 1.0f,
float tickInterval = 0.0f,
int damagePerTick = 0)
{
Type = type;
Duration = duration;
Intensity = intensity;
TickInterval = tickInterval;
DamagePerTick = damagePerTick;
}
/// <summary>
/// 更新效果(每帧调用)
/// 返回本帧是否触发了 tick(用于毒伤等周期性效果)
/// </summary>
public bool Update(float delta)
{
_elapsed += delta;
if (IsExpired) return false;
if (TickInterval > 0)
{
_tickTimer += delta;
if (_tickTimer >= TickInterval)
{
_tickTimer -= TickInterval;
return true; // 触发了一次 tick
}
}
return false;
}
}GDScript
## 效果类型枚举
enum EffectType {
FREEZE, ## 冰冻减速
POISON, ## 持续毒伤
STUN, ## 眩晕
VULNERABLE ## 易伤
}
## 状态效果 —— 施加在怪物身上的"负面状态"
class_name StatusEffect
var type: EffectType
var duration: float
var intensity: float
var tick_interval: float
var damage_per_tick: int
var _elapsed: float = 0.0
var _tick_timer: float = 0.0
## 效果是否过期
var is_expired: bool:
get: return _elapsed >= duration
func _init(
effect_type: EffectType,
eff_duration: float,
eff_intensity: float = 1.0,
eff_tick_interval: float = 0.0,
eff_damage_per_tick: int = 0
) -> void:
type = effect_type
duration = eff_duration
intensity = eff_intensity
tick_interval = eff_tick_interval
damage_per_tick = eff_damage_per_tick
## 更新效果
## 返回是否触发了 tick
func update(delta: float) -> bool:
_elapsed += delta
if is_expired:
return false
if tick_interval > 0:
_tick_timer += delta
if _tick_timer >= tick_interval:
_tick_timer -= tick_interval
return true
return false怪物效果管理器
将效果系统集成到怪物中,让怪物能够接收和处理各种状态效果。
C
using Godot;
using System.Collections.Generic;
/// <summary>
/// 怪物效果管理器组件
/// 挂载到怪物上,管理所有施加的效果
/// </summary>
public partial class MonsterEffectManager : Node
{
private readonly List<StatusEffect> _activeEffects = new();
private Monster _monster;
// 效果导致的属性修正值
private float _speedModifier = 1.0f;
private int _damageModifier = 0; // 额外受到的伤害百分比
public override void _Ready()
{
_monster = GetParent<Monster>();
}
public override void _Process(double delta)
{
if (_monster == null || !_monster.IsAlive) return;
// 更新所有效果
for (int i = _activeEffects.Count - 1; i >= 0; i--)
{
var effect = _activeEffects[i];
bool ticked = effect.Update((float)delta);
// 毒伤 tick
if (ticked && effect.Type == EffectType.Poison)
{
_monster.TakeDamage(effect.DamagePerTick);
}
// 移除过期效果
if (effect.IsExpired)
{
RemoveEffect(effect);
_activeEffects.RemoveAt(i);
}
}
// 应用速度修正
_monster.MoveSpeed = _monster.MoveSpeed * _speedModifier;
}
/// <summary>
/// 施加一个效果
/// </summary>
public void ApplyEffect(StatusEffect effect)
{
// 同类型效果不叠加,只刷新持续时间
var existing = _activeEffects.Find(
e => e.Type == effect.Type);
if (existing != null)
{
existing.Duration = effect.Duration;
return;
}
_activeEffects.Add(effect);
ApplyEffectModifiers(effect);
PlayEffectVisual(effect);
}
/// <summary>
/// 应用效果的属性修正
/// </summary>
private void ApplyEffectModifiers(StatusEffect effect)
{
switch (effect.Type)
{
case EffectType.Freeze:
_speedModifier *= effect.Intensity;
break;
case EffectType.Vulnerable:
_damageModifier += Mathf.RoundToInt(
effect.Intensity * 50);
break;
}
}
/// <summary>
/// 移除效果并恢复属性
/// </summary>
private void RemoveEffect(StatusEffect effect)
{
switch (effect.Type)
{
case EffectType.Freeze:
_speedModifier /= effect.Intensity;
break;
case EffectType.Vulnerable:
_damageModifier -= Mathf.RoundToInt(
effect.Intensity * 50);
break;
}
}
/// <summary>
/// 播放效果视觉
/// </summary>
private void PlayEffectVisual(StatusEffect effect)
{
switch (effect.Type)
{
case EffectType.Freeze:
// 蓝色色调
_monster.Modulate = new Color(0.5f, 0.8f, 1.0f);
break;
case EffectType.Poison:
// 绿色色调
_monster.Modulate = new Color(0.5f, 1.0f, 0.5f);
break;
case EffectType.Stun:
// 黄色闪烁
break;
}
}
/// <summary>
/// 获取额外的伤害加成(易伤效果)
/// </summary>
public int GetDamageBonus()
{
return _damageModifier;
}
}GDScript
extends Node
class_name MonsterEffectManager
## 怪物效果管理器组件
## 挂载到怪物上,管理所有施加的效果
var _active_effects: Array[StatusEffect] = []
var _monster: Monster = null
var _speed_modifier: float = 1.0
var _damage_modifier: int = 0
func _ready() -> void:
_monster = get_parent() as Monster
func _process(delta: float) -> void:
if not _monster or not _monster.is_alive:
return
# 更新所有效果
var i: int = _active_effects.size() - 1
while i >= 0:
var effect: StatusEffect = _active_effects[i]
var ticked: bool = effect.update(delta)
# 毒伤 tick
if ticked and effect.type == EffectType.POISON:
_monster.take_damage(effect.damage_per_tick)
# 移除过期效果
if effect.is_expired:
_remove_effect(effect)
_active_effects.remove_at(i)
i -= 1
## 施加效果
func apply_effect(effect: StatusEffect) -> void:
# 同类型不叠加,刷新持续时间
for existing in _active_effects:
if existing.type == effect.type:
existing.duration = effect.duration
return
_active_effects.append(effect)
_apply_effect_modifiers(effect)
_play_effect_visual(effect)
## 应用属性修正
func _apply_effect_modifiers(effect: StatusEffect) -> void:
match effect.type:
EffectType.FREEZE:
_speed_modifier *= effect.intensity
EffectType.VULNERABLE:
_damage_modifier += roundi(effect.intensity * 50)
## 移除效果
func _remove_effect(effect: StatusEffect) -> void:
match effect.type:
EffectType.FREEZE:
_speed_modifier /= effect.intensity
EffectType.VULNERABLE:
_damage_modifier -= roundi(effect.intensity * 50)
## 播放效果视觉
func _play_effect_visual(effect: StatusEffect) -> void:
match effect.type:
EffectType.FREEZE:
_monster.modulate = Color(0.5, 0.8, 1.0)
EffectType.POISON:
_monster.modulate = Color(0.5, 1.0, 0.5)
## 获取额外伤害加成
func get_damage_bonus() -> int:
return _damage_modifier冰冻塔实现
冰冻塔是一种特殊塔,攻击附带冰冻减速效果。
C
/// <summary>
/// 冰冻塔 —— 攻击附带冰冻效果
/// </summary>
public partial class IceTower : Tower
{
[Export] public float FreezeDuration { get; set; } = 2.0f;
[Export] public float FreezeIntensity { get; set; } = 0.4f;
public override void _Ready()
{
BaseDamage = 8;
AttackRange = 140.0f;
FireRate = 1.5f;
PurchaseCost = 150;
base._Ready();
}
protected override void Fire(Monster target)
{
LookAt(target.GlobalPosition);
base.Fire(target);
// 额外施加冰冻效果
var effectManager = target.GetNodeOrNull<MonsterEffectManager>(
"MonsterEffectManager");
if (effectManager != null)
{
effectManager.ApplyEffect(new StatusEffect(
EffectType.Freeze,
FreezeDuration,
FreezeIntensity
));
}
}
}GDScript
extends Tower
class_name IceTower
## 冰冻塔 —— 攻击附带冰冻效果
## 冰冻持续时间
@export var freeze_duration: float = 2.0
## 冰冻强度
@export var freeze_intensity: float = 0.4
func _ready() -> void:
base_damage = 8
attack_range = 140.0
fire_rate = 1.5
purchase_cost = 150
super._ready()
func _fire(target: Monster) -> void:
look_at(target.global_position)
super._fire(target)
# 施加冰冻效果
var effect_manager = target.get_node_or_null(
"MonsterEffectManager"
) as MonsterEffectManager
if effect_manager:
effect_manager.apply_effect(StatusEffect.new(
EffectType.FREEZE,
freeze_duration,
freeze_intensity
))塔组合效果
当特定类型的塔互相靠近放置时,可以获得额外的增益效果。这鼓励玩家思考塔的布局而不仅仅是"哪里空就放哪里"。
组合规则表
| 组合 | 条件 | 效果 |
|---|---|---|
| 冰火组合 | 冰塔+火箭塔距离<3格 | 火箭伤害+20%,目标被减速 |
| 风雷组合 | 风扇塔+雷达塔距离<3格 | 减速效果增强30% |
| 双瓶组合 | 两座瓶子塔距离<2格 | 攻击速度+15% |
C
using Godot;
using System.Collections.Generic;
/// <summary>
/// 塔组合检测器 —— 检测并应用塔之间的组合效果
/// </summary>
public partial class TowerComboDetector : Node
{
/// <summary>组合检测距离(像素)</summary>
[Export] public float ComboDistance { get; set; } = 192.0f;
private readonly Dictionary<Tower, List<string>> _activeCombos
= new();
public override void _Process(double delta)
{
var towers = GetTree().GetNodesInGroup("towers");
CheckAllCombos(towers);
}
/// <summary>
/// 检查所有塔的组合
/// </summary>
private void CheckAllCombos(Godot.Collections.Array towers)
{
// 先清除所有旧的组合效果
ClearAllCombos();
for (int i = 0; i < towers.Count; i++)
{
for (int j = i + 1; j < towers.Count; j++)
{
if (towers[i] is Tower a && towers[j] is Tower b)
{
CheckCombo(a, b);
}
}
}
}
/// <summary>
/// 检查两个塔之间的组合
/// </summary>
private void CheckCombo(Tower a, Tower b)
{
float dist = a.GlobalPosition.DistanceTo(
b.GlobalPosition);
if (dist > ComboDistance) return;
// 冰火组合
if ((a is IceTower && b is RocketTower)
|| (a is RocketTower && b is IceTower))
{
ApplyIceFireCombo(a is IceTower ? a : b,
a is RocketTower ? a : b);
}
// 风雷组合
if ((a is FanTower && b is RadarTower)
|| (a is RadarTower && b is FanTower))
{
ApplyWindRadarCombo(a is FanTower ? a : b,
a is RadarTower ? a : b);
}
}
private void ApplyIceFireCombo(Tower ice, Tower rocket)
{
// 简化:通过修改属性来实现组合效果
GD.Print("冰火组合激活!");
}
private void ApplyWindRadarCombo(Tower fan, Tower radar)
{
GD.Print("风雷组合激活!");
}
private void ClearAllCombos()
{
_activeCombos.Clear();
}
}GDScript
extends Node
class_name TowerComboDetector
## 塔组合检测器 —— 检测并应用塔之间的组合效果
## 组合检测距离
@export var combo_distance: float = 192.0
func _process(_delta: float) -> void:
var towers = get_tree().get_nodes_in_group("towers")
_check_all_combos(towers)
## 检查所有塔的组合
func _check_all_combos(towers: Array) -> void:
for i in range(towers.size()):
for j in range(i + 1, towers.size()):
if towers[i] is Tower a and towers[j] is Tower b:
_check_combo(a, b)
## 检查两个塔之间的组合
func _check_combo(a: Tower, b: Tower) -> void:
var dist: float = a.global_position.distance_to(b.global_position)
if dist > combo_distance:
return
# 冰火组合
if (a is IceTower and b is RocketTower) or (a is RocketTower and b is IceTower):
var ice: Tower = a if a is IceTower else b
var rocket: Tower = a if a is RocketTower else b
print("冰火组合激活!")
# 风雷组合
if (a is FanTower and b is RadarTower) or (a is RadarTower and b is FanTower):
var fan: Tower = a if a is FanTower else b
var radar: Tower = a if a is RadarTower else b
print("风雷组合激活!")本节小结
| 特殊机制 | 说明 |
|---|---|
| 障碍物 | 占据建塔位置,花金币或打掉后可用 |
| 状态效果 | 冰冻、毒伤、眩晕、易伤等 debuff |
| 冰冻塔 | 攻击附带减速效果 |
| 毒伤 | 周期性持续掉血 |
| 塔组合 | 特定塔靠近时获得额外增益 |
这些特殊机制让游戏的策略深度大幅提升。玩家不仅要考虑放什么塔,还要考虑放在哪里、和什么塔搭配。下一节我们将实现完整的游戏界面。
