10. 打磨与发布
2026/4/14大约 7 分钟
长沙麻将——打磨与发布
计分规则完善
长沙麻将的计分涉及多个维度:基础分、番数、自摸加底、扎鸟加番。本章我们将完善计分系统,实现完整的结算流程。
计分公式
最终得分 = 基础分 x 总番数
其中:
- 基础分 = 1分(可以自定义)
- 总番数 = 牌型番 + 自摸加底 + 扎鸟加番各情况的分数分配
| 情况 | 胡牌者得分 | 点炮者扣分 | 其他玩家 |
|---|---|---|---|
| 点炮 | +基础分 x 番数 | -基础分 x 番数 | 不变 |
| 自摸 | +基础分 x 番数 x 3 | 各扣 基础分 x 番数 | 各扣 基础分 x 番数 |
C
using Godot;
using System;
/// <summary>
/// 长沙麻将计分系统
/// </summary>
public partial class ScoreSystem : Node
{
/// <summary>基础分</summary>
public const int BASE_SCORE = 1;
/// <summary>累计分数(四个玩家)</summary>
private int[] _totalScores = new int[4];
/// <summary>本局得分变化</summary>
private int[] _roundChanges = new int[4];
public override void _Ready()
{
// 重置分数
Array.Clear(_totalScores, 0, 4);
GD.Print("[ScoreSystem] 计分系统初始化完成");
}
/// <summary>
/// 计算本局得分
/// </summary>
/// <param name="result">胡牌结果</param>
/// <param name="winner">胡牌者</param>
/// <param name="loser">点炮者(-1表示自摸)</param>
/// <returns>四个玩家的得分变化数组</returns>
public int[] CalculateRoundScore(WinResult result, int winner, int loser)
{
Array.Clear(_roundChanges, 0, 4);
int totalFan = result.Fan;
int scorePerPlayer = BASE_SCORE * totalFan;
if (result.IsSelfDraw)
{
// 自摸:其他三家各付一份
for (int i = 0; i < 4; i++)
{
if (i == winner) continue;
_roundChanges[i] -= scorePerPlayer;
}
_roundChanges[winner] += scorePerPlayer * 3;
}
else
{
// 点炮:只有点炮者付
_roundChanges[loser] -= scorePerPlayer;
_roundChanges[winner] += scorePerPlayer;
}
// 更新累计分数
for (int i = 0; i < 4; i++)
{
_totalScores[i] += _roundChanges[i];
}
// 打印本局结果
GD.Print("[ScoreSystem] === 本局结算 ===");
GD.Print($"[ScoreSystem] 胡牌者: 玩家{winner}");
GD.Print($"[ScoreSystem] 牌型: {result.PatternName}");
GD.Print($"[ScoreSystem] 总番数: {totalFan}番");
if (!result.IsSelfDraw)
{
GD.Print($"[ScoreSystem] 点炮者: 玩家{loser}");
}
GD.Print("[ScoreSystem] 得分变化:");
for (int i = 0; i < 4; i++)
{
string sign = _roundChanges[i] >= 0 ? "+" : "";
GD.Print($" 玩家{i}: {sign}{_roundChanges[i]} " +
$"(累计: {_totalScores[i]})");
}
return _roundChanges;
}
/// <summary>
/// 获取累计分数
/// </summary>
public int[] GetTotalScores()
{
return (int[])_totalScores.Clone();
}
/// <summary>
/// 获取本局得分变化
/// </summary>
public int[] GetRoundChanges()
{
return (int[])_roundChanges.Clone();
}
/// <summary>
/// 重置所有分数
/// </summary>
public void ResetScores()
{
Array.Clear(_totalScores, 0, 4);
Array.Clear(_roundChanges, 0, 4);
GD.Print("[ScoreSystem] 分数已重置");
}
}GDScript
extends Node
## 基础分
const BASE_SCORE: int = 1
## 累计分数(四个玩家)
var _total_scores: Array = [0, 0, 0, 0]
## 本局得分变化
var _round_changes: Array = [0, 0, 0, 0]
func _ready() -> void:
_total_scores = [0, 0, 0, 0]
print("[ScoreSystem] 计分系统初始化完成")
## 计算本局得分
## result: 胡牌结果, winner: 胡牌者, loser: 点炮者(-1表示自摸)
## 返回四个玩家的得分变化数组
func calculate_round_score(result, winner: int, loser: int) -> Array:
_round_changes = [0, 0, 0, 0]
var total_fan: int = result.fan
var score_per_player: int = BASE_SCORE * total_fan
if result.is_self_draw:
# 自摸:其他三家各付一份
for i in range(4):
if i == winner:
continue
_round_changes[i] -= score_per_player
_round_changes[winner] += score_per_player * 3
else:
# 点炮:只有点炮者付
_round_changes[loser] -= score_per_player
_round_changes[winner] += score_per_player
# 更新累计分数
for i in range(4):
_total_scores[i] += _round_changes[i]
# 打印本局结果
print("[ScoreSystem] === 本局结算 ===")
print("[ScoreSystem] 胡牌者: 玩家%d" % winner)
print("[ScoreSystem] 牌型: %s" % result.pattern_name)
print("[ScoreSystem] 总番数: %d番" % total_fan)
if not result.is_self_draw:
print("[ScoreSystem] 点炮者: 玩家%d" % loser)
print("[ScoreSystem] 得分变化:")
for i in range(4):
var sign: String = "+" if _round_changes[i] >= 0 else ""
print(" 玩家%d: %s%d (累计: %d)" % [
i, sign, _round_changes[i], _total_scores[i]
])
return _round_changes.duplicate()
## 获取累计分数
func get_total_scores() -> Array:
return _total_scores.duplicate()
## 获取本局得分变化
func get_round_changes() -> Array:
return _round_changes.duplicate()
## 重置所有分数
func reset_scores() -> void:
_total_scores = [0, 0, 0, 0]
_round_changes = [0, 0, 0, 0]
print("[ScoreSystem] 分数已重置")结算界面完善
完整结算流程
当有人胡牌或流局时,需要:
- 播放胡牌/流局特效
- 计算分数
- 显示结算弹窗
- 玩家选择继续或退出
C
/// <summary>
/// 结算管理器
/// 处理胡牌/流局后的结算流程
/// </summary>
public partial class SettlementManager : Node
{
private GameManager _gameManager;
private ScoreSystem _scoreSystem;
private WinChecker _winChecker;
private ResultPopup _resultPopup;
private WinEffect _winEffect;
public override void _Ready()
{
_gameManager = GetNode<GameManager>("/root/Main/GameManager");
_scoreSystem = GetNode<ScoreSystem>("/root/Main/ScoreSystem");
_winChecker = GetNode<WinChecker>("/root/Main/WinChecker");
_resultPopup = GetNode<ResultPopup>("/root/Main/Table/ResultPopup");
_winEffect = GetNode<WinEffect>("/root/Main/Table/WinEffect");
// 连接信号
_gameManager.PlayerWin += OnPlayerWin;
_gameManager.GameDraw += OnGameDraw;
_resultPopup.ContinuePressed += OnContinue;
_resultPopup.ExitPressed += OnExit;
}
/// <summary>
/// 处理胡牌
/// </summary>
private async void OnPlayerWin(int winner, bool isSelfDraw)
{
GD.Print($"[Settlement] 玩家{winner} 胡牌!自摸:{isSelfDraw}");
// 获取胡牌信息
var hand = _gameManager.GetHand(winner);
var melds = _gameManager.GetMelds(winner);
// 判断胡牌类型
var result = _winChecker.CheckWin(hand, melds, isSelfDraw, winner, null);
// 扎鸟
var birds = _gameManager.ZhaNiao(winner, isSelfDraw);
result.BirdBonus = _gameManager.CalculateBirdBonus(birds, hand, winner);
result.Fan += result.BirdBonus;
// 计算得分
int loser = isSelfDraw ? -1 : _gameManager.LastDiscardPlayer;
var scoreChanges = _scoreSystem.CalculateRoundScore(result, winner, loser);
// 播放胡牌特效
string winText = isSelfDraw ? "自摸!" : "胡了!";
await _winEffect.PlayWinEffect(winText, isSelfDraw);
// 显示结算弹窗
_resultPopup.ShowResult(result, winner, isSelfDraw, scoreChanges);
}
/// <summary>
/// 处理流局
/// </summary>
private void OnGameDraw()
{
GD.Print("[Settlement] 流局!");
MahjongAudioManager.Instance.PlayLiuju();
_resultPopup.ShowDraw();
}
/// <summary>
/// 继续下一局
/// </summary>
private void OnContinue()
{
_resultPopup.Visible = false;
// 庄家轮转(胡牌者做下一局庄家,或流局则庄家不变)
// 简化处理:庄家始终为玩家0
_gameManager.StartNewRound();
}
/// <summary>
/// 退出游戏
/// </summary>
private void OnExit()
{
_resultPopup.Visible = false;
// 显示最终总分
var scores = _scoreSystem.GetTotalScores();
GD.Print("[Settlement] === 最终总分 ===");
for (int i = 0; i < 4; i++)
{
GD.Print($" 玩家{i}: {scores[i]}分");
}
// 返回主菜单(或退出游戏)
GetTree().ChangeSceneToFile("res://scenes/MainMenu.tscn");
}
}GDScript
extends Node
var _game_manager: GameManager
var _score_system: ScoreSystem
var _win_checker: WinChecker
var _result_popup: ResultPopup
var _win_effect: WinEffect
func _ready() -> void:
_game_manager = get_node("/root/Main/GameManager")
_score_system = get_node("/root/Main/ScoreSystem")
_win_checker = get_node("/root/Main/WinChecker")
_result_popup = get_node("/root/Main/Table/ResultPopup")
_win_effect = get_node("/root/Main/Table/WinEffect")
# 连接信号
_game_manager.player_win.connect(_on_player_win)
_game_manager.game_draw.connect(_on_game_draw)
_result_popup.continue_pressed.connect(_on_continue)
_result_popup.exit_pressed.connect(_on_exit)
## 处理胡牌
func _on_player_win(winner: int, is_self_draw: bool) -> void:
print("[Settlement] 玩家%d 胡牌!自摸:%s" % [winner, is_self_draw])
# 获取胡牌信息
var hand: Array = _game_manager.get_hand(winner)
var melds: Array = _game_manager.get_melds(winner)
# 判断胡牌类型
var result = _win_checker.check_win(hand, melds, is_self_draw, winner, null)
# 扎鸟
var birds = _game_manager.zha_niao(winner, is_self_draw)
result.bird_bonus = _game_manager.calculate_bird_bonus(birds, hand, winner)
result.fan += result.bird_bonus
# 计算得分
var loser: int = -1 if is_self_draw else _game_manager.last_discard_player
var score_changes = _score_system.calculate_round_score(result, winner, loser)
# 播放胡牌特效
var win_text: String = "自摸!" if is_self_draw else "胡了!"
await _win_effect.play_win_effect(win_text, is_self_draw)
# 显示结算弹窗
_result_popup.show_result(result, winner, is_self_draw, score_changes)
## 处理流局
func _on_game_draw() -> void:
print("[Settlement] 流局!")
MahjongAudioManager.instance.play_liuju()
_result_popup.show_draw()
## 继续下一局
func _on_continue() -> void:
_result_popup.visible = false
_game_manager.start_new_round()
## 退出游戏
func _on_exit() -> void:
_result_popup.visible = false
# 显示最终总分
var scores = _score_system.get_total_scores()
print("[Settlement] === 最终总分 ===")
for i in range(4):
print(" 玩家%d: %d分" % [i, scores[i]])
# 返回主菜单
get_tree().change_scene_to_file("res://scenes/MainMenu.tscn")发布前检查
功能检查清单
| 检查项 | 说明 | 状态 |
|---|---|---|
| 发牌正确 | 每人13张,庄家14张 | [ ] |
| 摸牌出牌 | 流程正常,无死循环 | [ ] |
| 吃牌 | 只能吃上家,顺子正确 | [ ] |
| 碰牌 | 任意玩家可碰,优先级正确 | [ ] |
| 杠牌 | 明杠/暗杠/补杠均可 | [ ] |
| 胡牌判断 | 基本胡/七小对/清一色正确 | [ ] |
| 自摸加底 | 自摸分数翻倍 | [ ] |
| 扎鸟 | 翻牌加番正确 | [ ] |
| 计分 | 分数计算和分配正确 | [ ] |
| AI出牌 | 三个难度正常工作 | [ ] |
| 流局 | 牌墙摸完正确流局 | [ ] |
| 结算界面 | 显示正确的胡牌信息 | [ ] |
| 音效 | 所有音效正常播放 | [ ] |
| 动画 | 出牌/碰杠/胡牌动画流畅 | [ ] |
性能检查
| 检查项 | 目标 | 状态 |
|---|---|---|
| 内存使用 | < 200MB | [ ] |
| CPU占用 | < 30% | [ ] |
| 帧率 | 稳定60FPS | [ ] |
| 长时间运行 | 无内存泄漏 | [ ] |
导出配置
| 平台 | 配置要点 |
|---|---|
| Windows | 窗口1280x720,兼容模式渲染器 |
| Android | 横屏锁定,触摸适配 |
| Web | 压缩资源,减少加载时间 |
项目回顾
经过10章的学习,我们已经完成了一个完整的长沙麻将游戏。以下是各章节的成果回顾:
| 章节 | 内容 | 核心技术 |
|---|---|---|
| 1. 核心玩法 | 长沙麻将规则介绍 | 游戏设计 |
| 2. 项目搭建 | 场景结构、数据结构 | Tile结构体、Meld类 |
| 3. 牌面系统 | 108张牌生成和渲染 | 资源管理、手牌排序 |
| 4. 发牌与摸牌 | 洗牌、发牌、摸牌出牌 | Fisher-Yates算法、状态机 |
| 5. 吃碰杠 | 吃碰杠检测和执行 | 牌型匹配、优先级处理 |
| 6. 胡牌判断 | 基本胡型、特殊牌型、扎鸟 | 递归算法、番数计算 |
| 7. AI玩家 | 三个难度的出牌策略 | 启发式评估、听牌计算 |
| 8. 游戏界面 | 四人牌桌、结算弹窗 | 复杂UI布局 |
| 9. 音效与特效 | 麻将音效、牌面动画、胡牌特效 | Tween动画、粒子系统 |
| 10. 打磨与发布 | 计分完善、结算流程、导出 | 完整游戏循环 |
恭喜你完成了长沙麻将的全部教程!这个项目覆盖了麻将游戏开发的核心技术——牌型数据结构、胡牌判断算法、AI决策系统、多人交互逻辑等。希望你在学习过程中不仅掌握了技术,也感受到了长沙麻将的独特魅力。
