6. 关卡递进
2026/4/14大约 7 分钟
休闲益智——关卡递进
三消游戏有一个让人欲罢不能的设计:每一关都有明确的目标。不是无止境地消方块,而是"在 30 步内得到 3000 分"或者"在 25 步内消除 50 个红色方块"。有了目标,玩家就有了方向感;有了步数限制,玩家就有了紧迫感。再加上"差一步就过关"的刺激感——这就是关卡系统的魔力。
本章你将学到
- 设计关卡目标系统(分数目标、消除特定颜色)
- 实现步数限制和失败判定
- 实现星级评价(1-3星)
- 关卡解锁机制
- 用 Resource 资源文件配置关卡数据
关卡目标系统
目标类型
三消游戏的关卡目标不止一种,不同目标带来不同的策略:
| 目标类型 | 说明 | 策略 |
|---|---|---|
| 分数目标 | 在步数内达到指定分数 | 尽量触发连击,追求高分消除 |
| 消除指定颜色 | 消除特定颜色的方块 N 个 | 优先消除目标颜色的方块 |
| 收集特定物品 | 让特殊物品掉到底部 | 关注底部区域,不让物品卡住 |
| 清除障碍 | 消除覆盖在方块上的障碍 | 先处理障碍密集的区域 |
本教程先实现最基础的分数目标和消除指定颜色两种。
关卡数据结构
用 Godot 的 Resource 来存储关卡配置,就像给每一关写一张"任务卡"。
C
using Godot;
/// <summary>
/// 关卡配置资源 —— 每一关的"任务卡"。
/// 就像考卷上的题目:做多少步、要达到多少分、消除什么颜色的方块。
/// </summary>
[GlobalClass]
public partial class LevelConfig : Resource
{
/// <summary>关卡编号</summary>
[Export] public int LevelId { get; set; } = 1;
/// <summary>关卡名称</summary>
[Export] public string LevelName { get; set; } = "第一关";
/// <summary>允许的最大步数</summary>
[Export] public int MaxMoves { get; set; } = 30;
/// <summary>棋盘行数</summary>
[Export] public int BoardRows { get; set; } = 8;
/// <summary>棋盘列数</summary>
[Export] public int BoardCols { get; set; } = 8;
/// <summary>方块种类数量</summary>
[Export] public int PieceTypes { get; set; } = 5;
// --- 目标配置 ---
/// <summary>目标类型:0=分数目标,1=消除指定颜色</summary>
[Export] public int GoalType { get; set; } = 0;
/// <summary>分数目标值(GoalType=0 时使用)</summary>
[Export] public int TargetScore { get; set; } = 3000;
/// <summary>目标颜色编号(GoalType=1 时使用)</summary>
[Export] public int TargetPieceType { get; set; } = 1;
/// <summary>目标消除数量(GoalType=1 时使用)</summary>
[Export] public int TargetClearCount { get; set; } = 50;
// --- 星级评价阈值 ---
/// <summary>1星所需分数</summary>
[Export] public int Star1Score { get; set; } = 3000;
/// <summary>2星所需分数</summary>
[Export] public int Star2Score { get; set; } = 5000;
/// <summary>3星所需分数</summary>
[Export] public int Star3Score { get; set; } = 8000;
}GDScript
class_name LevelConfig
extends Resource
## 关卡配置资源 —— 每一关的"任务卡"
## 关卡编号
@export var level_id: int = 1
## 关卡名称
@export var level_name: String = "第一关"
## 允许的最大步数
@export var max_moves: int = 30
## 棋盘行数
@export var board_rows: int = 8
## 棋盘列数
@export var board_cols: int = 8
## 方块种类数量
@export var piece_types: int = 5
# --- 目标配置 ---
## 目标类型:0=分数目标,1=消除指定颜色
@export var goal_type: int = 0
## 分数目标值
@export var target_score: int = 3000
## 目标颜色编号
@export var target_piece_type: int = 1
## 目标消除数量
@export var target_clear_count: int = 50
# --- 星级评价阈值 ---
## 1星所需分数
@export var star1_score: int = 3000
## 2星所需分数
@export var star2_score: int = 5000
## 3星所需分数
@export var star3_score: int = 8000步数限制
步数限制是三消游戏的核心压力来源。每次有效交换(产生了匹配的交换)消耗一步。当步数用完时,检查是否达到了目标。
C
/// <summary>
/// 关卡管理器 —— 管理当前关卡的进度。
/// 就像一场考试的监考老师:记录你做了多少步、得了多少分、
/// 目标达到了没有。
/// </summary>
public partial class LevelManager : Node
{
public static LevelManager Instance { get; private set; }
[Signal]
public delegate void MovesUpdatedEventHandler(int remaining);
[Signal]
public delegate void GoalProgressEventHandler(float progress);
[Signal]
public delegate void LevelCompleteEventHandler(int stars);
[Signal]
public delegate void LevelFailedEventHandler();
private LevelConfig _config;
private int _movesRemaining;
private int _score;
private int _clearedCount; // 已消除的目标方块数
/// <summary>剩余步数</summary>
public int MovesRemaining => _movesRemaining;
/// <summary>目标完成进度(0.0 到 1.0)</summary>
public float GoalProgress
{
get
{
if (_config.GoalType == 0)
{
return Mathf.Min(1.0f, (float)_score / _config.TargetScore);
}
else
{
return Mathf.Min(1.0f, (float)_clearedCount / _config.TargetClearCount);
}
}
}
public override void _Ready()
{
if (Instance != null && Instance != this)
{
QueueFree();
return;
}
Instance = this;
}
/// <summary>加载关卡配置并开始</summary>
public void StartLevel(LevelConfig config)
{
_config = config;
_movesRemaining = config.MaxMoves;
_score = 0;
_clearedCount = 0;
EmitSignal(SignalName.MovesUpdated, _movesRemaining);
EmitSignal(SignalName.GoalProgress, 0.0f);
}
/// <summary>消耗一步(每次有效交换时调用)</summary>
public void UseMove()
{
_movesRemaining--;
EmitSignal(SignalName.MovesUpdated, _movesRemaining);
// 步数用完,检查结果
if (_movesRemaining <= 0)
{
CheckLevelResult();
}
}
/// <summary>记录消除得分</summary>
public void AddScore(int points)
{
_score += points;
EmitSignal(SignalName.GoalProgress, GoalProgress);
// 分数模式下,实时检查是否达标
if (_config.GoalType == 0 && _score >= _config.TargetScore)
{
CompleteLevel();
}
}
/// <summary>记录消除的目标方块</summary>
public void AddClearedPiece(int pieceType)
{
if (_config.GoalType == 1 && pieceType == _config.TargetPieceType)
{
_clearedCount++;
EmitSignal(SignalName.GoalProgress, GoalProgress);
if (_clearedCount >= _config.TargetClearCount)
{
CompleteLevel();
}
}
}
/// <summary>检查关卡结果</summary>
private void CheckLevelResult()
{
if (GoalProgress >= 1.0f)
{
CompleteLevel();
}
else
{
EmitSignal(SignalName.LevelFailed);
}
}
/// <summary>过关——计算星级</summary>
private void CompleteLevel()
{
int stars = 1;
if (_score >= _config.Star2Score) stars = 2;
if (_score >= _config.Star3Score) stars = 3;
EmitSignal(SignalName.LevelComplete, stars);
}
}GDScript
extends Node
## 关卡管理器 —— 管理当前关卡的进度
@onready var instance: Node = self
signal moves_updated(remaining: int)
signal goal_progress(progress: float)
signal level_complete(stars: int)
signal level_failed()
var config: LevelConfig
var moves_remaining: int
var _score: int
var cleared_count: int
## 目标完成进度
func get_goal_progress() -> float:
if config.goal_type == 0:
return minf(1.0, float(_score) / float(config.target_score))
else:
return minf(1.0, float(cleared_count) / float(config.target_clear_count))
func _ready():
if instance and instance != self:
queue_free()
return
instance = self
## 加载关卡配置并开始
func start_level(cfg: LevelConfig) -> void:
config = cfg
moves_remaining = cfg.max_moves
_score = 0
cleared_count = 0
moves_updated.emit(moves_remaining)
goal_progress.emit(0.0)
## 消耗一步
func use_move() -> void:
moves_remaining -= 1
moves_updated.emit(moves_remaining)
if moves_remaining <= 0:
_check_level_result()
## 记录消除得分
func add_score(points: int) -> void:
_score += points
goal_progress.emit(get_goal_progress())
if config.goal_type == 0 and _score >= config.target_score:
_complete_level()
## 记录消除的目标方块
func add_cleared_piece(piece_type: int) -> void:
if config.goal_type == 1 and piece_type == config.target_piece_type:
cleared_count += 1
goal_progress.emit(get_goal_progress())
if cleared_count >= config.target_clear_count:
_complete_level()
## 检查关卡结果
func _check_level_result() -> void:
if get_goal_progress() >= 1.0:
_complete_level()
else:
level_failed.emit()
## 过关
func _complete_level() -> void:
var stars: int = 1
if _score >= config.star2_score:
stars = 2
if _score >= config.star3_score:
stars = 3
level_complete.emit(stars)星级评价
星级评价让玩家有"再来一次"的动力——即使过关了,只拿到 1 星也会让人不服气:"我明明可以拿到 3 星的!"
星级评价的分数间隔要合理:
| 星级 | 分数 | 占3星比例 | 玩家心理 |
|---|---|---|---|
| 1星 | 3000 | 37.5% | "好险,差一点就失败了" |
| 2星 | 5000 | 62.5% | "还行,但我觉得可以更好" |
| 3星 | 8000 | 100% | "完美!我是三消大师!" |
关卡解锁机制
最简单的解锁方式是线性解锁:通过第 N 关才能玩第 N+1 关。
C
/// <summary>
/// 关卡进度管理器 —— 记录玩家解锁到了哪一关、每关拿了多少星。
/// 用 JSON 文件保存进度,下次打开游戏自动读取。
/// </summary>
public partial class LevelProgress : Node
{
private const string SavePath = "user://level_progress.json";
private int _maxUnlockedLevel = 1;
private Dictionary<int, int> _levelStars = new();
public int MaxUnlockedLevel => _maxUnlockedLevel;
public override void _Ready()
{
LoadProgress();
}
/// <summary>检查某关是否已解锁</summary>
public bool IsLevelUnlocked(int levelId)
{
return levelId <= _maxUnlockedLevel;
}
/// <summary>过关后记录星级,解锁下一关</summary>
public void CompleteLevel(int levelId, int stars)
{
// 记录最高星级
if (!_levelStars.ContainsKey(levelId) || _levelStars[levelId] < stars)
{
_levelStars[levelId] = stars;
}
// 解锁下一关
if (levelId >= _maxUnlockedLevel)
{
_maxUnlockedLevel = levelId + 1;
}
SaveProgress();
}
private void SaveProgress()
{
var data = new Godot.Collections.Dictionary
{
["max_unlocked"] = _maxUnlockedLevel,
["stars"] = new Godot.Collections.Dictionary(_levelStars)
};
var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write);
file.StoreString(Json.Stringify(data));
file.Close();
}
private void LoadProgress()
{
if (!FileAccess.FileExists(SavePath)) return;
var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Read);
var data = Json.ParseString(file.GetAsText()).AsGodotDictionary();
file.Close();
_maxUnlockedLevel = (int)data["max_unlocked"];
var stars = data["stars"].AsGodotDictionary();
foreach (var key in stars.Keys)
{
_levelStars[(int)key] = (int)stars[key];
}
}
}GDScript
extends Node
## 关卡进度管理器 —— 记录玩家解锁到了哪一关、每关拿了多少星
const SAVE_PATH: String = "user://level_progress.json"
var max_unlocked_level: int = 1
var level_stars: Dictionary = {}
func _ready():
_load_progress()
## 检查某关是否已解锁
func is_level_unlocked(level_id: int) -> bool:
return level_id <= max_unlocked_level
## 过关后记录星级,解锁下一关
func complete_level(level_id: int, stars: int) -> void:
if level_id not in level_stars or level_stars[level_id] < stars:
level_stars[level_id] = stars
if level_id >= max_unlocked_level:
max_unlocked_level = level_id + 1
_save_progress()
func _save_progress() -> void:
var data: Dictionary = {
"max_unlocked": max_unlocked_level,
"stars": level_stars
}
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify(data))
file.close()
func _load_progress() -> void:
if not FileAccess.file_exists(SAVE_PATH):
return
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
var data = JSON.parse_string(file.get_as_text())
file.close()
max_unlocked_level = data.get("max_unlocked", 1)
level_stars = data.get("stars", {})下一章预告
关卡系统让游戏有了目标和进度感。下一章我们将加入让游戏更有趣的元素——特殊方块(炸弹、彩虹、行列清除),它们能让一次消除的威力翻好几倍。
