8. 营救人质机制
2026/4/13大约 10 分钟
营救人质机制
营救人质是赤色要塞最独特的玩法——你不是在单纯地杀敌,你还有任务去救人。每当你开车靠近被困的人质,他们就会跑上你的车。这一章我们来实现完整的营救系统。
营救流程
原版的营救流程
地图上有建筑/房间 → 里面有人质 → 开车靠近 → 人质跑出来上车 → HUD更新人数 → 运到撤离点我们的实现步骤
- 人质放置:在地图上特定位置放置人质(建筑内或空地上)
- 触发营救:吉普车靠近人质一定范围
- 上车动画:人质跑向吉普车并消失
- HUD更新:界面显示已救人数
- 营救奖励:救人达到一定数量,解锁武器升级
- 撤离结算:到达撤离点时,根据人质数量计算额外得分
人质场景
人质节点结构
Hostage (CharacterBody3D) ← 人质节点
├── MeshInstance3D ← 人质模型(临时用胶囊体)
├── CollisionShape3D ← 碰撞形状
├── RescueArea (Area3D) ← 营救触发区域
│ └── CollisionShape3D ← 触发范围
└── AnimationPlayer ← 动画控制器人质脚本
C
using Godot;
/// <summary>
/// 人质 - 等待玩家营救
/// </summary>
public partial class Hostage : CharacterBody3D
{
[Export] public float RescueRange { get; set; } = 3.0f; // 营救触发距离
[Export] public float RunSpeed { get; set; } = 5.0f; // 跑向车辆的速度
[Export] public int PointValue { get; set; } = 500; // 营救得分
public enum HostageState { Waiting, Rescued, RunningToVehicle, OnBoard }
public HostageState State { get; private set; } = HostageState.Waiting;
private Node3D _rescueVehicle; // 营救车辆引用
private AnimationPlayer _animPlayer;
public override void _Ready()
{
_animPlayer = GetNodeOrNull<AnimationPlayer>("AnimationPlayer");
// 监听营救区域
var rescueArea = GetNode<Area3D>("RescueArea");
rescueArea.BodyEntered += OnBodyNearby;
}
public override void _PhysicsProcess(double delta)
{
switch (State)
{
case HostageState.Waiting:
OnWaiting();
break;
case HostageState.RunningToVehicle:
OnRunningToVehicle((float)delta);
break;
case HostageState.OnBoard:
// 已上车,隐藏自己
break;
}
}
/// <summary>
/// 等待状态:原地待命,偶尔播放待机动画
/// </summary>
private void OnWaiting()
{
// 人质在等待时可以做一些小动作
// 比如左右看、蹲下等
}
/// <summary>
/// 有物体靠近时检查是不是玩家的车
/// </summary>
private void OnBodyNearby(Node3D body)
{
if (State != HostageState.Waiting) return;
if (body.IsInGroup("player"))
{
State = HostageState.RunningToVehicle;
_rescueVehicle = body;
// 通知人质管理器
var manager = GetNodeOrNull<HostageManager>("/root/HostageManager");
manager?.OnHostageRescuing(this);
}
}
/// <summary>
/// 跑向车辆
/// </summary>
private void OnRunningToVehicle(float delta)
{
if (_rescueVehicle == null)
{
State = HostageState.Waiting;
return;
}
Vector3 targetPos = _rescueVehicle.GlobalPosition;
Vector3 direction = (targetPos - GlobalPosition).Normalized();
float distance = GlobalPosition.DistanceTo(targetPos);
// 面向车辆
LookAt(new Vector3(targetPos.X, GlobalPosition.Y, targetPos.Z), Vector3.Up);
// 跑向车辆
Velocity = direction * RunSpeed;
MoveAndSlide();
// 接近车辆时上车
if (distance < 1.5f)
{
BoardVehicle();
}
}
/// <summary>
/// 上车
/// </summary>
private void BoardVehicle()
{
State = HostageState.OnBoard;
// 隐藏模型
Visible = false;
// 关闭碰撞
SetDeferred("collision_layer", 0);
SetDeferred("collision_mask", 0);
// 通知人质管理器
var manager = GetNodeOrNull<HostageManager>("/root/HostageManager");
manager?.OnHostageBoarded(this);
GD.Print("[Hostage] 人质已上车!");
// 播放欢呼音效
// TODO: 第9章实现
}
/// <summary>
/// 被敌人杀害(如果敌人攻击了人质附近)
/// </summary>
public void Kill()
{
State = HostageState.Waiting; // 重置状态
GD.Print("[Hostage] 人质被杀害了!");
// 缩小消失
var tween = CreateTween();
tween.TweenProperty(this, "scale", Vector3.Zero, 0.5f);
tween.TweenCallback(Callable.From(() => QueueFree()));
}
}GDScript
# 人质 - 等待玩家营救
extends CharacterBody3D
@export var rescue_range: float = 3.0 # 营救触发距离
@export var run_speed: float = 5.0 # 跑向车辆的速度
@export var point_value: int = 500 # 营救得分
enum HostageState { WAITING, RESCUED, RUNNING_TO_VEHICLE, ON_BOARD }
var state: HostageState = HostageState.WAITING
var _rescue_vehicle: Node3D # 营救车辆引用
var _anim_player: AnimationPlayer
func _ready():
_anim_player = $AnimationPlayer
# 监听营救区域
$RescueArea.body_entered.connect(_on_body_nearby)
func _physics_process(delta):
match state:
HostageState.WAITING:
_on_waiting()
HostageState.RUNNING_TO_VEHICLE:
_on_running_to_vehicle(delta)
HostageState.ON_BOARD:
pass # 已上车
## 等待状态
func _on_waiting():
pass # 人质在等待时可以做小动作
## 有物体靠近时检查是不是玩家的车
func _on_body_nearby(body: Node3D):
if state != HostageState.WAITING:
return
if body.is_in_group("player"):
state = HostageState.RUNNING_TO_VEHICLE
_rescue_vehicle = body
# 通知人质管理器
var manager = get_node_or_null("/root/HostageManager")
if manager:
manager.on_hostage_rescuing(self)
## 跑向车辆
func _on_running_to_vehicle(delta: float):
if _rescue_vehicle == null:
state = HostageState.WAITING
return
var target_pos = _rescue_vehicle.global_position
var direction = (target_pos - global_position).normalized()
var distance = global_position.distance_to(target_pos)
# 面向车辆
look_at(Vector3(target_pos.x, global_position.y, target_pos.z), Vector3.UP)
# 跑向车辆
velocity = direction * run_speed
move_and_slide()
# 接近车辆时上车
if distance < 1.5:
_board_vehicle()
## 上车
func _board_vehicle():
state = HostageState.ON_BOARD
# 隐藏模型
visible = false
# 关闭碰撞
set_deferred("collision_layer", 0)
set_deferred("collision_mask", 0)
# 通知人质管理器
var manager = get_node_or_null("/root/HostageManager")
if manager:
manager.on_hostage_boarded(self)
print("[Hostage] 人质已上车!")
## 被敌人杀害
func kill():
state = HostageState.WAITING
print("[Hostage] 人质被杀害了!")
var tween = create_tween()
tween.tween_property(self, "scale", Vector3.ZERO, 0.5)
tween.tween_callback(queue_free)人质管理器
人质管理器跟踪所有的人质状态,处理营救奖励逻辑。
C
using Godot;
using System.Collections.Generic;
/// <summary>
/// 人质管理器 - 管理所有人质的状态和营救奖励
/// 作为 AutoLoad 全局单例使用
/// </summary>
public partial class HostageManager : Node
{
[Signal]
public delegate void HostageCountChangedEventHandler(int rescued, int total);
[Signal]
public delegate void WeaponUpgradedEventHandler(string weaponName);
// 营救奖励阈值
[Export] public int[] UpgradeThresholds { get; set; } = { 3, 6, 10 };
private int _totalHostages; // 关卡中的总人质数
private int _rescuedCount; // 已营救数量
private int _currentWeaponLevel; // 当前武器等级
private readonly List<Hostage> _hostages = new();
/// <summary>
/// 初始化关卡的人质
/// </summary>
public void InitializeForLevel()
{
_hostages.Clear();
_rescuedCount = 0;
_currentWeaponLevel = 0;
// 查找当前关卡中所有人质
foreach (var node in GetTree().GetNodesInGroup("hostage"))
{
if (node is Hostage hostage)
{
_hostages.Add(hostage);
}
}
_totalHostages = _hostages.Count;
EmitSignal(SignalName.HostageCountChanged, 0, _totalHostages);
}
/// <summary>
/// 人质开始跑向车辆
/// </summary>
public void OnHostageRescuing(Hostage hostage)
{
GD.Print($"[HostageManager] 发现人质!正在营救...");
}
/// <summary>
/// 人质已上车
/// </summary>
public void OnHostageBoarded(Hostage hostage)
{
_rescuedCount++;
EmitSignal(SignalName.HostageCountChanged, _rescuedCount, _totalHostages);
GD.Print($"[HostageManager] 已营救 {_rescuedCount}/{_totalHostages} 名人质");
// 检查是否达到武器升级条件
CheckUpgrade();
}
/// <summary>
/// 检查营救奖励
/// </summary>
private void CheckUpgrade()
{
if (_currentWeaponLevel >= UpgradeThresholds.Length) return;
if (_rescuedCount >= UpgradeThresholds[_currentWeaponLevel])
{
_currentWeaponLevel++;
string weaponName = GetWeaponName(_currentWeaponLevel);
EmitSignal(SignalName.WeaponUpgraded, weaponName);
GD.Print($"[HostageManager] 武器升级!获得:{weaponName}");
}
}
private string GetWeaponName(int level)
{
return level switch
{
1 => "双管机枪",
2 => "火箭弹",
3 => "导弹",
_ => "未知武器"
};
}
/// <summary>
/// 获取当前武器等级
/// </summary>
public int GetWeaponLevel() => _currentWeaponLevel;
/// <summary>
/// 获取已营救数量
/// </summary>
public int GetRescuedCount() => _rescuedCount;
/// <summary>
/// 获取总人质数
/// </summary>
public int GetTotalCount() => _totalHostages;
}GDScript
# 人质管理器 - 管理所有人质的状态和营救奖励
# 作为 AutoLoad 全局单例使用
extends Node
signal hostage_count_changed(rescued: int, total: int)
signal weapon_upgraded(weapon_name: String)
# 营救奖励阈值
@export var upgrade_thresholds: Array[int] = [3, 6, 10]
var _total_hostages: int = 0 # 关卡中的总人质数
var _rescued_count: int = 0 # 已营救数量
var _current_weapon_level: int = 0 # 当前武器等级
var _hostages: Array = []
## 初始化关卡的人质
func initialize_for_level():
_hostages.clear()
_rescued_count = 0
_current_weapon_level = 0
# 查找当前关卡中所有人质
for node in get_tree().get_nodes_in_group("hostage"):
var hostage = node
if hostage:
_hostages.append(hostage)
_total_hostages = _hostages.size()
hostage_count_changed.emit(0, _total_hostages)
## 人质开始跑向车辆
func on_hostage_rescuing(_hostage):
print("[HostageManager] 发现人质!正在营救...")
## 人质已上车
func on_hostage_boarded(_hostage):
_rescued_count += 1
hostage_count_changed.emit(_rescued_count, _total_hostages)
print("[HostageManager] 已营救 %d/%d 名人质" % [_rescued_count, _total_hostages])
# 检查是否达到武器升级条件
_check_upgrade()
## 检查营救奖励
func _check_upgrade():
if _current_weapon_level >= upgrade_thresholds.size():
return
if _rescued_count >= upgrade_thresholds[_current_weapon_level]:
_current_weapon_level += 1
var weapon_name = _get_weapon_name(_current_weapon_level)
weapon_upgraded.emit(weapon_name)
print("[HostageManager] 武器升级!获得:", weapon_name)
func _get_weapon_name(level: int) -> String:
match level:
1: return "双管机枪"
2: return "火箭弹"
3: return "导弹"
_: return "未知武器"
## 获取当前武器等级
func get_weapon_level() -> int:
return _current_weapon_level
## 获取已营救数量
func get_rescued_count() -> int:
return _rescued_count
## 获取总人质数
func get_total_count() -> int:
return _total_hostages营救奖励系统
救人不是白救的——每救一定数量的人质,你的武器就会升级:
| 累计营救 | 奖励 | 效果 |
|---|---|---|
| 3人 | 双管机枪 | 同时发射2发子弹,散布角度小 |
| 6人 | 火箭弹 | 手雷替换为火箭弹,射程更远 |
| 10人 | 导弹 | 锁定制导导弹,自动追踪敌人 |
武器升级应用到射击系统
在上一章的 WeaponManager 中加入武器等级判断:
C
/// <summary>
/// 根据武器等级发射不同子弹
/// </summary>
private void Fire()
{
int weaponLevel = 0;
var hostageManager = GetNodeOrNull<HostageManager>("/root/HostageManager");
if (hostageManager != null)
weaponLevel = hostageManager.GetWeaponLevel();
switch (weaponLevel)
{
case 0: // 基础机枪
FireSingleBullet();
break;
case 1: // 双管机枪
FireDoubleBullet();
break;
case 2: // 火箭弹
FireSingleBullet();
break;
case 3: // 导弹(自动追踪)
FireHomingMissile();
break;
}
}
private void FireSingleBullet()
{
if (BulletScene == null) return;
var jeep = GetParent().GetParent<Node3D>();
Vector3 shootDir = -jeep.GlobalTransform.Basis.Z;
var bullet = BulletScene.Instantiate<Bullet>();
GetTree().CurrentScene.AddChild(bullet);
bullet.GlobalPosition = GlobalPosition;
bullet.SetDirection(shootDir);
}
private void FireDoubleBullet()
{
if (BulletScene == null) return;
var jeep = GetParent().GetParent<Node3D>();
Vector3 shootDir = -jeep.GlobalTransform.Basis.Z;
// 发射两发,略微散布
for (int i = -1; i <= 1; i += 2)
{
var bullet = BulletScene.Instantiate<Bullet>();
GetTree().CurrentScene.AddChild(bullet);
bullet.GlobalPosition = GlobalPosition + Vector3.Right * i * 0.3f;
bullet.SetDirection(shootDir);
}
}GDScript
## 根据武器等级发射不同子弹
func fire():
var weapon_level = 0
var hostage_manager = get_node_or_null("/root/HostageManager")
if hostage_manager:
weapon_level = hostage_manager.get_weapon_level()
match weapon_level:
0: # 基础机枪
_fire_single_bullet()
1: # 双管机枪
_fire_double_bullet()
2: # 火箭弹
_fire_single_bullet()
3: # 导弹(自动追踪)
_fire_homing_missile()
func _fire_single_bullet():
if bullet_scene == null:
return
var jeep = get_parent().get_parent() as Node3D
var shoot_dir = -jeep.global_transform.basis.z
var bullet = bullet_scene.instantiate()
get_tree().current_scene.add_child(bullet)
bullet.global_position = global_position
bullet.set_direction(shoot_dir)
func _fire_double_bullet():
if bullet_scene == null:
return
var jeep = get_parent().get_parent() as Node3D
var shoot_dir = -jeep.global_transform.basis.z
# 发射两发,略微散布
for i in [-1, 1]:
var bullet = bullet_scene.instantiate()
get_tree().current_scene.add_child(bullet)
bullet.global_position = global_position + Vector3.RIGHT * i * 0.3
bullet.set_direction(shoot_dir)人质HUD显示
在游戏画面上显示人质营救进度:
┌─────────────────────────────┐
│ ❤️ x3 🧑 2/8 💣 ■■□□ │
│ │
│ 游戏画面 │
│ │
│ │
│ ┌────┐ │
│ │小地图│ │
│ └────┘ │
└─────────────────────────────┘
HUD说明:
- ❤️ x3:剩余生命
- 🧑 2/8:已救2人/总共8人
- 💣 ■■□□:手雷冷却进度HUD 节点结构
GameHUD (Control)
├── TopBar (HBoxContainer)
│ ├── LifeIcon (TextureRect) ← 生命图标
│ ├── LifeLabel (Label) ← 生命数量
│ ├── HostageIcon (TextureRect) ← 人质图标
│ ├── HostageLabel (Label) ← "2/8"
│ └── WeaponIcon (TextureRect) ← 当前武器图标
├── CenterMessages (CenterContainer) ← 居中消息(如"武器升级!")
│ └── MessageLabel (Label)
├── GrenadeBar (ProgressBar) ← 手雷冷却
└── MiniMapContainer ← 小地图HUD 脚本
C
using Godot;
/// <summary>
/// 游戏HUD - 显示生命、人质数量、武器等信息
/// </summary>
public partial class GameHUD : Control
{
private Label _hostageLabel;
private Label _lifeLabel;
private Label _messageLabel;
private ProgressBar _grenadeBar;
public override void _Ready()
{
_hostageLabel = GetNode<Label>("TopBar/HostageLabel");
_lifeLabel = GetNode<Label>("TopBar/LifeLabel");
_messageLabel = GetNode<Label>("CenterMessages/MessageLabel");
_grenadeBar = GetNode<ProgressBar>("GrenadeBar");
// 连接人质管理器信号
var hostageManager = GetNodeOrNull<HostageManager>("/root/HostageManager");
if (hostageManager != null)
{
hostageManager.HostageCountChanged += OnHostageCountChanged;
hostageManager.WeaponUpgraded += OnWeaponUpgraded;
}
}
private void OnHostageCountChanged(int rescued, int total)
{
_hostageLabel.Text = $"{rescued}/{total}";
}
private void OnWeaponUpgraded(string weaponName)
{
ShowMessage($"武器升级:{weaponName}!");
}
/// <summary>
/// 显示居中提示消息
/// </summary>
public void ShowMessage(string text, float duration = 2.0f)
{
_messageLabel.Text = text;
_messageLabel.Visible = true;
// 渐入渐出动画
var tween = CreateTween();
tween.TweenProperty(_messageLabel, "modulate", Colors.White, 0.3f);
tween.TweenInterval(duration);
tween.TweenProperty(_messageLabel, "modulate", new Color(1, 1, 1, 0), 0.5f);
tween.TweenCallback(Callable.From(() => _messageLabel.Visible = false));
}
/// <summary>
/// 更新手雷冷却进度
/// </summary>
public void UpdateGrenadeCooldown(float progress)
{
_grenadeBar.Value = progress * 100;
}
}GDScript
# 游戏HUD - 显示生命、人质数量、武器等信息
extends Control
var _hostage_label: Label
var _life_label: Label
var _message_label: Label
var _grenade_bar: ProgressBar
func _ready():
_hostage_label = $TopBar/HostageLabel
_life_label = $TopBar/LifeLabel
_message_label = $CenterMessages/MessageLabel
_grenade_bar = $GrenadeBar
# 连接人质管理器信号
var hostage_manager = get_node_or_null("/root/HostageManager")
if hostage_manager:
hostage_manager.hostage_count_changed.connect(_on_hostage_count_changed)
hostage_manager.weapon_upgraded.connect(_on_weapon_upgraded)
func _on_hostage_count_changed(rescued: int, total: int):
_hostage_label.text = "%d/%d" % [rescued, total]
func _on_weapon_upgraded(weapon_name: String):
show_message("武器升级:%s!" % weapon_name)
## 显示居中提示消息
func show_message(text: String, duration: float = 2.0):
_message_label.text = text
_message_label.visible = true
# 渐入渐出动画
var tween = create_tween()
tween.tween_property(_message_label, "modulate", Color.WHITE, 0.3)
tween.tween_interval(duration)
tween.tween_property(_message_label, "modulate", Color(1, 1, 1, 0), 0.5)
tween.tween_callback(func(): _message_label.visible = false)
## 更新手雷冷却进度
func update_grenade_cooldown(progress: float):
_grenade_bar.value = progress * 100人质在建筑中的放置
在赤色要塞中,人质通常被困在建筑物里。当建筑物被摧毁后,人质才会出现。
带人质的建筑
C
using Godot;
/// <summary>
/// 带人质的建筑 - 摧毁后释放人质
/// </summary>
public partial class HostageBuilding : Destructible
{
[Export] public PackedScene HostageScene { get; set; } // 人质场景
[Export] public int HostageCount { get; set; } = 1; // 建筑内人质数量
[Export] public float SpawnRadius { get; set; } = 2.0f; // 生成半径
protected override void Destroy()
{
// 先释放人质
for (int i = 0; i < HostageCount; i++)
{
SpawnHostage();
}
// 再执行基类的销毁逻辑
base.Destroy();
}
private void SpawnHostage()
{
if (HostageScene == null) return;
var hostage = HostageScene.Instantiate<Node3D>();
GetTree().CurrentScene.AddChild(hostage);
// 在建筑附近随机位置生成
float angle = (float)GD.RandRange(0, Mathf.Tau);
float dist = (float)GD.RandRange(0.5f, SpawnRadius);
Vector3 offset = new(Mathf.Cos(angle) * dist, 0, Mathf.Sin(angle) * dist);
hostage.GlobalPosition = GlobalPosition + offset;
}
}GDScript
# 带人质的建筑 - 摧毁后释放人质
extends "res://scripts/map/destructible.gd"
@export var hostage_scene: PackedScene # 人质场景
@export var hostage_count: int = 1 # 建筑内人质数量
@export var spawn_radius: float = 2.0 # 生成半径
func _destroy():
# 先释放人质
for i in range(hostage_count):
_spawn_hostage()
# 再执行基类的销毁逻辑
super._destroy()
func _spawn_hostage():
if hostage_scene == null:
return
var hostage = hostage_scene.instantiate()
get_tree().current_scene.add_child(hostage)
# 在建筑附近随机位置生成
var angle = randf_range(0, TAU)
var dist = randf_range(0.5, spawn_radius)
var offset = Vector3(cos(angle) * dist, 0, sin(angle) * dist)
hostage.global_position = global_position + offset撤离时的人质结算
到达撤离点时,根据营救的人质数量计算额外奖励:
| 营救比例 | 评价 | 额外得分 |
|---|---|---|
| 100% | S | +5000分 |
| 75%~99% | A | +3000分 |
| 50%~74% | B | +1500分 |
| 25%~49% | C | +500分 |
| <25% | D | +0分 |
本章检查清单
下一章
核心玩法全部实现了,接下来给游戏加上音效和视觉特效,让它"有声有色"!
