5. 计分与连击
2026/4/14大约 5 分钟
休闲益智——计分与连击
想象你正在玩三消游戏:消了三个红色,得了 100 分;紧接着新掉下来的方块又凑成三个蓝色,又是 100 分——但屏幕上显示的不是 100,而是 200!因为这是连击。继续连下去:300、500、800……分数越涨越快,你的心跳也跟着加速。
这就是计分与连击系统的魔力——它把简单的"消除方块"变成了一场让人肾上腺素飙升的连锁狂欢。
本章你将学到
- 设计基础计分规则
- 实现连击倍率系统
- 处理连锁反应的分数计算
- 制作分数飘字动画
- 通过信号系统让 UI 实时更新分数
基础计分规则
三消游戏的计分规则通常是这样的:
| 消除数量 | 基础得分 | 说明 |
|---|---|---|
| 3 个 | 100 分 | 最基本的消除 |
| 4 个 | 200 分 | 多消一个,分数翻倍 |
| 5 个 | 500 分 | 难度大增,奖励更高 |
| 6+ 个 | 1000 分 | 极其罕见,给大奖励 |
为什么不是简单的"每个方块 33 分"?
如果每个方块固定值,玩家就没有动力去追求多消。递增的奖励让玩家会主动寻找"一次消四个""一次消五个"的机会,这增加了策略深度。
连击倍率
连击(Combo)就是"一次交换触发了多次消除"。第一次消除是 1 连击,紧接着又消除是 2 连击,以此类推。连击越高,倍率越大。
| 连击次数 | 倍率 | 3消得分 | 4消得分 |
|---|---|---|---|
| 1x | 1.0 倍 | 100 | 200 |
| 2x | 1.5 倍 | 150 | 300 |
| 3x | 2.0 倍 | 200 | 400 |
| 4x | 2.5 倍 | 250 | 500 |
| 5x+ | 3.0 倍 | 300 | 600 |
C
using Godot;
/// <summary>
/// 计分管理器 —— 负责计算每次消除的分数,管理连击倍率。
/// 就像超市的收银台:每消除一组方块就"结一次账"。
/// </summary>
public partial class ScoreManager : Node
{
public static ScoreManager Instance { get; private set; }
[Signal]
public delegate void ScoreAddedEventHandler(int points, Vector2 position);
[Signal]
public delegate void ComboChangedEventHandler(int combo);
[Signal]
public delegate void TotalScoreChangedEventHandler(int totalScore);
private int _totalScore = 0;
private int _currentCombo = 0;
/// <summary>当前总分数</summary>
public int TotalScore => _totalScore;
/// <summary>当前连击数</summary>
public int CurrentCombo => _currentCombo;
public override void _Ready()
{
if (Instance != null && Instance != this)
{
QueueFree();
return;
}
Instance = this;
}
/// <summary>
/// 计算一组匹配消除的得分。
/// points = 基础分 x 消除数量奖励 x 连击倍率
/// </summary>
public int CalculateMatchScore(List<Vector2I> matchedPositions, Vector2 screenPos)
{
int count = matchedPositions.Count;
// 基础分:根据消除数量递增
int basePoints = count switch
{
3 => 100,
4 => 200,
5 => 500,
_ => 1000
};
// 连击倍率
_currentCombo++;
float comboMultiplier = GetComboMultiplier(_currentCombo);
int finalPoints = (int)(basePoints * comboMultiplier);
// 累加总分
_totalScore += finalPoints;
// 发送信号
EmitSignal(SignalName.ScoreAdded, finalPoints, screenPos);
EmitSignal(SignalName.ComboChanged, _currentCombo);
EmitSignal(SignalName.TotalScoreChanged, _totalScore);
return finalPoints;
}
/// <summary>
/// 重置连击计数(一次交换操作结束后调用)
/// </summary>
public void ResetCombo()
{
_currentCombo = 0;
EmitSignal(SignalName.ComboChanged, 0);
}
/// <summary>获取连击倍率</summary>
private float GetComboMultiplier(int combo)
{
return combo switch
{
1 => 1.0f,
2 => 1.5f,
3 => 2.0f,
4 => 2.5f,
_ => 3.0f
};
}
}GDScript
extends Node
## 计分管理器 —— 负责计算每次消除的分数,管理连击倍率
@onready var instance: Node = self
signal score_added(points: int, position: Vector2)
signal combo_changed(combo: int)
signal total_score_changed(total_score: int)
var total_score: int = 0
var current_combo: int = 0
func _ready():
if instance and instance != self:
queue_free()
return
instance = self
## 计算一组匹配消除的得分
func calculate_match_score(matched_positions: Array, screen_pos: Vector2) -> int:
var count: int = matched_positions.size()
# 基础分:根据消除数量递增
var base_points: int
match count:
3:
base_points = 100
4:
base_points = 200
5:
base_points = 500
_:
base_points = 1000
# 连击倍率
current_combo += 1
var combo_multiplier: float = _get_combo_multiplier(current_combo)
var final_points: int = int(base_points * combo_multiplier)
# 累加总分
total_score += final_points
# 发送信号
score_added.emit(final_points, screen_pos)
combo_changed.emit(current_combo)
total_score_changed.emit(total_score)
return final_points
## 重置连击计数
func reset_combo() -> void:
current_combo = 0
combo_changed.emit(0)
## 获取连击倍率
func _get_combo_multiplier(combo: int) -> float:
match combo:
1: return 1.0
2: return 1.5
3: return 2.0
4: return 2.5
_: return 3.0分数飘字动画
"分数飘字"就是消除时在方块位置上弹出一个数字(比如 "+100"),然后慢慢向上飘并淡出。这个小小的动画能极大地增强打击感。
C
/// <summary>
/// 在指定位置显示飘字分数。
/// 创建一个 Label 节点,设置文字、颜色、位置,
/// 然后用 Tween 让它向上飘并淡出,最后自动删除。
/// </summary>
public void ShowFloatingScore(int points, Vector2 position)
{
var label = new Label();
label.Text = $"+{points}";
label.HorizontalAlignment = HorizontalAlignment.Center;
// 根据分数大小设置不同颜色
label.AddThemeColorOverride(
"font_color",
points >= 500 ? new Color("#FFD700") : new Color("#FFFFFF")
);
// 根据连击数设置字体大小
int fontSize = Mathf.Min(24 + ScoreManager.Instance.CurrentCombo * 4, 48);
label.AddThemeFontSizeOverride("font_size", fontSize);
label.Position = position;
AddChild(label);
// Tween 动画:向上飘 + 淡出 + 缩小
var tween = CreateTween();
tween.SetParallel(true);
tween.TweenProperty(label, "position:y", position.Y - 60, 0.8f);
tween.TweenProperty(label, "modulate:a", 0.0f, 0.8f);
tween.TweenProperty(label, "scale", new Vector2(1.5f, 1.5f), 0.3f);
// 动画结束后自动删除
tween.Chain().TweenCallback(Callable.From(() => label.QueueFree()));
}GDScript
## 在指定位置显示飘字分数
func show_floating_score(points: int, position: Vector2) -> void:
var label = Label.new()
label.text = "+%d" % points
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
# 根据分数大小设置不同颜色
var color: Color = Color("#FFD700") if points >= 500 else Color("#FFFFFF")
label.add_theme_color_override("font_color", color)
# 根据连击数设置字体大小
var font_size: int = mini(24 + ScoreManager.instance.current_combo * 4, 48)
label.add_theme_font_size_override("font_size", font_size)
label.position = position
add_child(label)
# Tween 动画:向上飘 + 淡出
var tween = create_tween()
tween.set_parallel(true)
tween.tween_property(label, "position:y", position.y - 60, 0.8)
tween.tween_property(label, "modulate:a", 0.0, 0.8)
tween.tween_property(label, "scale", Vector2(1.5, 1.5), 0.3)
# 动画结束后自动删除
tween.chain().tween_callback(label.queue_free)连锁反应的分数处理
当一次交换触发了连锁反应时,每次消除的连击数都要递增。关键点在于:连击计数在一次完整的交换流程中持续累加,直到没有新的匹配时才重置。
玩家交换方块
→ 消除第1组(1连击,100分 x 1.0 = 100分)
→ 下落填充
→ 消除第2组(2连击,100分 x 1.5 = 150分)
→ 下落填充
→ 消除第3组(3连击,200分 x 2.0 = 400分)
→ 没有新匹配了
→ 重置连击为0
→ 本次交换总计获得:100 + 150 + 400 = 650分注意
连击倍率是对"单次消除"的分数乘算,不是对"总分数"乘算。上例中 3 连击的 2.0 倍只作用于第 3 次消除的 200 分基础分,不是作用于前面的 250 分。
下一章预告
分数系统做好了,但"得多少分才算过关"呢?这就需要关卡系统。下一章我们将实现关卡目标、步数限制和星级评价。
