7. 任务与关卡流程
2026/4/13大约 10 分钟
任务与关卡流程
赤色要塞不是一个没有目标的"杀杀杀"游戏——每个关卡都有明确的任务目标,完成目标后到达撤离点就算过关。这一章我们来搭建任务系统和关卡管理器。
关卡结构设计
原版赤色要塞的关卡结构
原版赤色要塞的每一关大概是这样的:
起点 ──→ 区域A(热身战)──→ 区域B(中等难度)──→ 区域C(Boss战)──→ 撤离点每个区域有特定的任务(消灭敌人、摧毁建筑、救人等),完成所有区域后到达终点即通关。
我们的关卡结构
关卡开始
│
├─ 任务1:清除前方敌人
├─ 任务2:摧毁碉堡
├─ 任务3:营救人质
├─ 任务4:摧毁敌方基地
│
└─ 到达撤离点 → 关卡结束 → 下一关任务类型
| 任务类型 | 说明 | 完成条件 | 示例 |
|---|---|---|---|
| 消灭敌人 | 杀死指定数量的敌人 | 敌人计数归零 | "消灭5名步兵" |
| 摧毁目标 | 摧毁指定建筑物 | 建筑物被摧毁 | "摧毁敌军碉堡" |
| 到达地点 | 到达指定位置 | 玩家进入目标区域 | "到达撤离点" |
| 营救人质 | 救出指定数量的人质 | 人质上车 | "营救3名人质" |
| 生存 | 在一段时间内存活 | 计时结束 | "坚守阵地30秒" |
| 护送 | 保护目标到达指定位置 | 目标到达终点 | "护送友军车辆" |
任务数据结构
用 Resource 定义任务数据
Godot 的 Resource 系统非常适合存储游戏数据。你可以把 Resource 理解为"数据容器"——它就像一张表格,定义了某个东西的各种属性。
C
using Godot;
/// <summary>
/// 任务数据 - 定义单个任务的属性
/// </summary>
[GlobalClass]
public partial class MissionData : Resource
{
[Export] public string MissionId { get; set; } = ""; // 任务ID
[Export] public string MissionName { get; set; } = ""; // 任务名称
[Export] public string Description { get; set; } = ""; // 任务描述
[Export] public MissionType Type { get; set; } = MissionType.KillEnemies;
[Export] public int TargetCount { get; set; } = 1; // 目标数量
[Export] public string TargetTag { get; set; } = ""; // 目标标签(如 "infantry"、"bunker")
[Export] public Vector3 TargetPosition { get; set; } = Vector3.Zero; // 目标位置(到达类任务)
[Export] public float TimeLimit { get; set; } = 0.0f; // 时间限制(0=无限制)
}
public enum MissionType
{
KillEnemies, // 消灭敌人
DestroyTarget, // 摧毁目标
ReachLocation, // 到达地点
RescueHostages, // 营救人质
Survive, // 生存
Escort // 护送
}GDScript
## 任务数据 - 定义单个任务的属性
class_name MissionData
extends Resource
@export var mission_id: String = "" # 任务ID
@export var mission_name: String = "" # 任务名称
@export var description: String = "" # 任务描述
@export var type: String = "kill_enemies" # 任务类型
@export var target_count: int = 1 # 目标数量
@export var target_tag: String = "" # 目标标签
@export var target_position: Vector3 = Vector3.ZERO # 目标位置
@export var time_limit: float = 0.0 # 时间限制(0=无限制)
# 任务类型常量
enum MissionType {
KILL_ENEMIES, # 消灭敌人
DESTROY_TARGET, # 摧毁目标
REACH_LOCATION, # 到达地点
RESCUE_HOSTAGES, # 营救人质
SURVIVE, # 生存
ESCORT # 护送
}关卡数据
C
using Godot;
/// <summary>
/// 关卡数据 - 定义一个关卡的所有信息
/// </summary>
[GlobalClass]
public partial class LevelData : Resource
{
[Export] public string LevelId { get; set; } = ""; // 关卡ID
[Export] public string LevelName { get; set; } = ""; // 关卡名称
[Export] public string ScenePath { get; set; } = ""; // 场景文件路径
[Export] public MissionData[] Missions { get; set; } // 任务列表
[Export] public Vector3 PlayerStartPos { get; set; } = Vector3.Zero; // 玩家起始位置
[Export] public Vector3 ExtractPoint { get; set; } = Vector3.Zero; // 撤离点位置
[Export] public string NextLevelId { get; set; } = ""; // 下一关ID
[Export] public string BgmPath { get; set; } = ""; // 背景音乐路径
}GDScript
## 关卡数据 - 定义一个关卡的所有信息
class_name LevelData
extends Resource
@export var level_id: String = "" # 关卡ID
@export var level_name: String = "" # 关卡名称
@export var scene_path: String = "" # 场景文件路径
@export var missions: Array[MissionData] = [] # 任务列表
@export var player_start_pos: Vector3 = Vector3.ZERO # 玩家起始位置
@export var extract_point: Vector3 = Vector3.Zero # 撤离点位置
@export var next_level_id: String = "" # 下一关ID
@export var bgm_path: String = "" # 背景音乐路径任务管理器
任务管理器负责跟踪所有任务的状态,并在任务完成时发出通知。
C
using Godot;
using System.Collections.Generic;
/// <summary>
/// 任务管理器 - 跟踪和管理当前关卡的所有任务
/// </summary>
public partial class MissionManager : Node
{
[Signal]
public delegate void MissionStartedEventHandler(string missionId, string missionName);
[Signal]
public delegate void MissionProgressEventHandler(string missionId, int current, int total);
[Signal]
public delegate void MissionCompletedEventHandler(string missionId, string missionName);
[Signal]
public delegate void AllMissionsCompletedEventHandler();
private readonly List<ActiveMission> _activeMissions = new();
private int _completedCount;
/// <summary>
/// 活跃任务(运行时的任务状态)
/// </summary>
public class ActiveMission
{
public MissionData Data;
public int CurrentProgress;
public bool IsCompleted;
}
/// <summary>
/// 加载关卡的所有任务
/// </summary>
public void LoadMissions(MissionData[] missions)
{
_activeMissions.Clear();
_completedCount = 0;
foreach (var missionData in missions)
{
var active = new ActiveMission
{
Data = missionData,
CurrentProgress = 0,
IsCompleted = false
};
_activeMissions.Add(active);
EmitSignal(SignalName.MissionStarted, missionData.MissionId, missionData.MissionName);
}
}
/// <summary>
/// 通知任务进度(如击杀敌人、摧毁目标)
/// </summary>
public void NotifyProgress(string tag, int amount = 1)
{
foreach (var mission in _activeMissions)
{
if (mission.IsCompleted) continue;
if (mission.Data.TargetTag != tag) continue;
mission.CurrentProgress += amount;
EmitSignal(SignalName.MissionProgress,
mission.Data.MissionId,
mission.CurrentProgress,
mission.Data.TargetCount);
// 检查是否完成
if (mission.CurrentProgress >= mission.Data.TargetCount)
{
CompleteMission(mission);
}
}
}
private void CompleteMission(ActiveMission mission)
{
mission.IsCompleted = true;
_completedCount++;
EmitSignal(SignalName.MissionCompleted, mission.Data.MissionId, mission.Data.MissionName);
GD.Print($"[MissionManager] 任务完成:{mission.Data.MissionName}");
// 检查是否所有任务都完成了
if (_completedCount >= _activeMissions.Count)
{
EmitSignal(SignalName.AllMissionsCompleted);
GD.Print("[MissionManager] 所有任务完成!前往撤离点!");
}
}
/// <summary>
/// 获取当前任务列表(给 HUD 用)
/// </summary>
public List<ActiveMission> GetActiveMissions() => _activeMissions;
/// <summary>
/// 检查是否所有任务都完成了
/// </summary>
public bool AreAllMissionsCompleted() => _completedCount >= _activeMissions.Count;
}GDScript
## 任务管理器 - 跟踪和管理当前关卡的所有任务
extends Node
signal mission_started(mission_id: String, mission_name: String)
signal mission_progress(mission_id: String, current: int, total: int)
signal mission_completed(mission_id: String, mission_name: String)
signal all_missions_completed()
var _active_missions: Array = []
var _completed_count: int = 0
## 加载关卡的所有任务
func load_missions(missions: Array):
_active_missions.clear()
_completed_count = 0
for mission_data in missions:
var active = {
"data": mission_data,
"current_progress": 0,
"is_completed": false
}
_active_missions.append(active)
mission_started.emit(mission_data.mission_id, mission_data.mission_name)
## 通知任务进度(如击杀敌人、摧毁目标)
func notify_progress(tag: String, amount: int = 1):
for mission in _active_missions:
if mission.is_completed:
continue
if mission.data.target_tag != tag:
continue
mission.current_progress += amount
mission_progress.emit(mission.data.mission_id, mission.current_progress, mission.data.target_count)
# 检查是否完成
if mission.current_progress >= mission.data.target_count:
_complete_mission(mission)
func _complete_mission(mission: Dictionary):
mission.is_completed = true
_completed_count += 1
mission_completed.emit(mission.data.mission_id, mission.data.mission_name)
print("[MissionManager] 任务完成:", mission.data.mission_name)
# 检查是否所有任务都完成了
if _completed_count >= _active_missions.size():
all_missions_completed.emit()
print("[MissionManager] 所有任务完成!前往撤离点!")
## 获取当前任务列表(给 HUD 用)
func get_active_missions() -> Array:
return _active_missions
## 检查是否所有任务都完成了
func are_all_missions_completed() -> bool:
return _completed_count >= _active_missions.size()关卡管理器
关卡管理器负责关卡的加载、切换和结束判定。
C
using Godot;
/// <summary>
/// 关卡管理器 - 管理关卡加载和切换
/// 作为 AutoLoad 全局单例使用
/// </summary>
public partial class LevelManager : Node
{
[Signal]
public delegate void LevelLoadedEventHandler(string levelName);
[Signal]
public delegate void LevelCompletedEventHandler(string levelName);
private LevelData _currentLevel;
private int _currentLevelIndex;
private LevelData[] _allLevels;
public override void _Ready()
{
// 加载关卡列表(从配置文件或硬编码)
LoadLevelList();
}
/// <summary>
/// 加载关卡列表
/// </summary>
private void LoadLevelList()
{
_allLevels = new LevelData[]
{
// 第1关:丛林前哨
new LevelData
{
LevelId = "level_01",
LevelName = "丛林前哨",
ScenePath = "res://scenes/maps/Level01.tscn",
PlayerStartPos = new Vector3(0, 0.5f, 0),
ExtractPoint = new Vector3(0, 0.5f, 100),
NextLevelId = "level_02",
Missions = new MissionData[]
{
new() { MissionId = "m1_1", MissionName = "消灭前哨守卫", Type = MissionType.KillEnemies, TargetCount = 5, TargetTag = "infantry" },
new() { MissionId = "m1_2", MissionName = "摧毁哨塔", Type = MissionType.DestroyTarget, TargetCount = 2, TargetTag = "turret" },
new() { MissionId = "m1_3", MissionName = "到达撤离点", Type = MissionType.ReachLocation, TargetTag = "extract_point" }
}
},
// 第2关:河流要塞
new LevelData
{
LevelId = "level_02",
LevelName = "河流要塞",
ScenePath = "res://scenes/maps/Level02.tscn",
PlayerStartPos = new Vector3(0, 0.5f, 0),
ExtractPoint = new Vector3(0, 0.5f, 150),
NextLevelId = "level_03",
Missions = new MissionData[]
{
new() { MissionId = "m2_1", MissionName = "渡过桥梁", Type = MissionType.ReachLocation, TargetTag = "bridge" },
new() { MissionId = "m2_2", MissionName = "摧毁河防碉堡", Type = MissionType.DestroyTarget, TargetCount = 3, TargetTag = "bunker" },
new() { MissionId = "m2_3", MissionName = "营救被困人质", Type = MissionType.RescueHostages, TargetCount = 4, TargetTag = "hostage" }
}
}
};
}
/// <summary>
/// 加载指定关卡
/// </summary>
public void LoadLevel(string levelId)
{
foreach (var level in _allLevels)
{
if (level.LevelId == levelId)
{
_currentLevel = level;
GetTree().ChangeSceneToFile(level.ScenePath);
EmitSignal(SignalName.LevelLoaded, level.LevelName);
return;
}
}
GD.PrintErr($"[LevelManager] 找不到关卡:{levelId}");
}
/// <summary>
/// 加载下一关
/// </summary>
public void LoadNextLevel()
{
if (_currentLevel == null || string.IsNullOrEmpty(_currentLevel.NextLevelId))
{
GD.Print("[LevelManager] 没有下一关了,游戏通关!");
// TODO: 显示通关画面
return;
}
LoadLevel(_currentLevel.NextLevelId);
}
/// <summary>
/// 关卡完成
/// </summary>
public void CompleteLevel()
{
if (_currentLevel == null) return;
EmitSignal(SignalName.LevelCompleted, _currentLevel.LevelName);
// 显示关卡结算画面
GetTree().ChangeSceneToFile("res://scenes/ui/LevelComplete.tscn");
}
/// <summary>
/// 获取当前关卡数据
/// </summary>
public LevelData GetCurrentLevel() => _currentLevel;
}GDScript
## 关卡管理器 - 管理关卡加载和切换
## 作为 AutoLoad 全局单例使用
extends Node
signal level_loaded(level_name: String)
signal level_completed(level_name: String)
var _current_level: LevelData
var _current_level_index: int = 0
var _all_levels: Array = []
func _ready():
_load_level_list()
## 加载关卡列表
func _load_level_list():
_all_levels = [
# 第1关:丛林前哨
LevelData.new(),
# ... 实际项目中用 JSON 或 Resource 文件加载
]
## 从JSON加载关卡数据
func load_levels_from_json(json_path: String):
var file = FileAccess.open(json_path, FileAccess.READ)
if file == null:
push_error("[LevelManager] 无法打开关卡文件:" + json_path)
return
var json = JSON.new()
var error = json.parse(file.get_as_text())
if error != OK:
push_error("[LevelManager] JSON 解析错误:" + json.get_error_message())
return
var data = json.data
_all_levels.clear()
for level_data in data:
var level = LevelData.new()
level.level_id = level_data.get("level_id", "")
level.level_name = level_data.get("level_name", "")
level.scene_path = level_data.get("scene_path", "")
level.player_start_pos = _parse_vector3(level_data.get("player_start_pos", "0,0.5,0"))
level.extract_point = _parse_vector3(level_data.get("extract_point", "0,0.5,100"))
level.next_level_id = level_data.get("next_level_id", "")
_all_levels.append(level)
## 加载指定关卡
func load_level(level_id: String):
for level in _all_levels:
if level.level_id == level_id:
_current_level = level
get_tree().change_scene_to_file(level.scene_path)
level_loaded.emit(level.level_name)
return
push_error("[LevelManager] 找不到关卡:" + level_id)
## 加载下一关
func load_next_level():
if _current_level == null or _current_level.next_level_id == "":
print("[LevelManager] 没有下一关了,游戏通关!")
return
load_level(_current_level.next_level_id)
## 关卡完成
func complete_level():
if _current_level == null:
return
level_completed.emit(_current_level.level_name)
get_tree().change_scene_to_file("res://scenes/ui/LevelComplete.tscn")
## 获取当前关卡数据
func get_current_level() -> LevelData:
return _current_level
## 解析Vector3字符串
func _parse_vector3(s: String) -> Vector3:
var parts = s.split(",")
if parts.size() >= 3:
return Vector3(float(parts[0]), float(parts[1]), float(parts[2]))
return Vector3.ZERO关卡JSON配置示例
用JSON文件存储关卡配置,比硬编码更灵活:
{
"levels": [
{
"level_id": "level_01",
"level_name": "丛林前哨",
"scene_path": "res://scenes/maps/Level01.tscn",
"player_start_pos": "0,0.5,0",
"extract_point": "0,0.5,100",
"next_level_id": "level_02",
"bgm_path": "res://assets/sounds/bgm/level01.ogg",
"missions": [
{
"mission_id": "m1_1",
"mission_name": "消灭前哨守卫",
"type": "kill_enemies",
"target_count": 5,
"target_tag": "infantry"
},
{
"mission_id": "m1_2",
"mission_name": "摧毁哨塔",
"type": "destroy_target",
"target_count": 2,
"target_tag": "turret"
}
]
},
{
"level_id": "level_02",
"level_name": "河流要塞",
"scene_path": "res://scenes/maps/Level02.tscn",
"player_start_pos": "0,0.5,0",
"extract_point": "0,0.5,150",
"next_level_id": "level_03",
"missions": [
{
"mission_id": "m2_1",
"mission_name": "渡过桥梁",
"type": "reach_location",
"target_tag": "bridge"
},
{
"mission_id": "m2_2",
"mission_name": "摧毁河防碉堡",
"type": "destroy_target",
"target_count": 3,
"target_tag": "bunker"
}
]
}
]
}撤离点实现
撤离点是关卡的终点,所有任务完成后进入撤离点即可通关。
C
using Godot;
/// <summary>
/// 撤离点 - 所有任务完成后进入即通关
/// </summary>
public partial class ExtractPoint : Area3D
{
[Export] public float Radius { get; set; } = 3.0f; // 触发半径
private bool _isActivated;
public override void _Ready()
{
BodyEntered += OnBodyEntered;
// 监听任务完成信号
var missionManager = GetNodeOrNull<MissionManager>("/root/MissionManager");
if (missionManager != null)
missionManager.AllMissionsCompleted += OnAllMissionsCompleted;
}
private void OnAllMissionsCompleted()
{
_isActivated = true;
// TODO: 播放"前往撤离点"的提示和闪烁动画
GD.Print("[ExtractPoint] 撤离点已激活!前往撤离点!");
}
private void OnBodyEntered(Node3D body)
{
if (!_isActivated) return;
if (!body.IsInGroup("player")) return;
// 关卡完成
var levelManager = GetNodeOrNull<LevelManager>("/root/LevelManager");
levelManager?.CompleteLevel();
}
}GDScript
# 撤离点 - 所有任务完成后进入即通关
extends Area3D
@export var radius: float = 3.0 # 触发半径
var _is_activated: bool = false
func _ready():
body_entered.connect(_on_body_entered)
# 监听任务完成信号
var mission_manager = get_node_or_null("/root/MissionManager")
if mission_manager:
mission_manager.all_missions_completed.connect(_on_all_missions_completed)
func _on_all_missions_completed():
_is_activated = true
# TODO: 播放"前往撤离点"的提示和闪烁动画
print("[ExtractPoint] 撤离点已激活!前往撤离点!")
func _on_body_entered(body: Node3D):
if not _is_activated:
return
if not body.is_in_group("player"):
return
# 关卡完成
var level_manager = get_node_or_null("/root/LevelManager")
if level_manager:
level_manager.complete_level()关卡结算画面
关卡完成后,显示结算画面,包含以下信息:
| 项目 | 说明 |
|---|---|
| 关卡名称 | 刚刚完成的关卡名字 |
| 用时 | 完成关卡花费的时间 |
| 消灭敌人 | 击杀敌人数 |
| 营救人质 | 救出人质数 |
| 得分 | 总分 |
| 评价 | S/A/B/C 根据表现评级 |
| 下一关按钮 | 点击进入下一关 |
多关卡流程图
┌─────────────────────────────────────────────────────┐
│ 主菜单 │
│ ┌───────┐ │
│ │开始游戏│ │
│ └───┬───┘ │
│ ↓ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │第1关 │───→│第2关 │───→│第3关 │───→│通关画面│ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
│ 每一关:加载场景 → 初始化任务 → 游戏进行 → 完成 │
└─────────────────────────────────────────────────────┘本章检查清单
下一章
关卡流程搭好了,接下来实现赤色要塞最经典的机制——营救人质!
