Unity中Firebase实时数据库玩家匹配场景跳转异常问题排查
Unity问答游戏匹配功能故障排查求助
我正在开发一款带玩家匹配(matchmaking)和多人游戏(multiplayer)功能的Unity问答游戏,已经配置好Firebase Auth和Firebase实时数据库。玩家点击开始按钮后会进入匹配队列并显示“搜索游戏”提示,但测试时发现:Player1点击开始后,Player2再点击开始,只有Player2能进入GamePlay场景,Player1始终停留在原场景。这是我第一次开发在线游戏,缺乏相关经验,希望能排查这个问题,后续还要实现分数、玩家名称等功能。
以下是我的MatchmakingManager代码:
using UnityEngine; using UnityEngine.SceneManagement; using Firebase; using Firebase.Database; using Firebase.Auth; using System; using System.Collections.Generic; public class MatchmakingManager : MonoBehaviour { private DatabaseReference databaseReference; private FirebaseAuth auth; private FirebaseUser currentUser; private bool isMatchmaking = false; private bool sceneTransitioned = false; void Start() { // Initialize Firebase components databaseReference = FirebaseDatabase.DefaultInstance.RootReference; auth = FirebaseAuth.DefaultInstance; currentUser = auth.CurrentUser; if (currentUser != null) { // Check if the current user is already in matchmaking CheckMatchmakingStatus(); } else { Debug.LogError("User is not logged in."); } } private void CheckMatchmakingStatus() { // Check if the current user is already in matchmaking databaseReference.Child("matchmaking").Child(currentUser.UserId).GetValueAsync().ContinueWith(task => { if (task.IsCompleted && !task.IsFaulted && !task.IsCanceled) { DataSnapshot snapshot = task.Result; if (snapshot != null && snapshot.Exists) { // User is already in matchmaking isMatchmaking = true; } else { // User is not in matchmaking isMatchmaking = false; } } else { Debug.LogError("Failed to check matchmaking status: " + task.Exception); } }); } public void StartMatchmaking() { if (!isMatchmaking) { // Add the current user to matchmaking databaseReference.Child("matchmaking").Child(currentUser.UserId).SetValueAsync(true).ContinueWith(task => { if (task.IsCompleted && !task.IsFaulted && !task.IsCanceled) { // Successfully added to matchmaking isMatchmaking = true; // Check for available matches FindMatch(); } else { Debug.LogError("Failed to start matchmaking: " + task.Exception); } }); } else { Debug.LogWarning("User is already in matchmaking."); } } public void CancelMatchmaking() { // Remove the player from the matchmaking pool string playerId = auth.CurrentUser.UserId; databaseReference.Child("matchmaking").Child(playerId).RemoveValueAsync().ContinueWith(task => { if (task.IsCompleted && !task.IsFaulted && !task.IsCanceled) { // Successfully removed from matchmaking isMatchmaking = false; } else { Debug.LogError("Failed to cancel matchmaking: " + task.Exception); } }); } private void FindMatch() { // Query the database for game rooms with two players databaseReference.Child("matchmaking").OrderByKey().LimitToFirst(2).GetValueAsync().ContinueWith(task => { if (task.IsCompleted && !task.IsFaulted && !task.IsCanceled) { DataSnapshot snapshot = task.Result; if (snapshot != null && snapshot.HasChildren && snapshot.ChildrenCount == 2) { // Found two players, remove them from matchmaking List<string> playerIds = new List<string>(); foreach (DataSnapshot playerSnapshot in snapshot.Children) { string playerId = playerSnapshot.Key; playerIds.Add(playerId); } // Create a game room for the matched players CreateGameRoom(playerIds[0], playerIds[1]); } else { // No available matches yet or insufficient players in the queue Debug.Log("No available matches yet or insufficient players in the queue, continuing waiting..."); } } else { Debug.LogError("Failed to find a match: " + task.Exception); } }); } private void CreateGameRoom(string player1Id, string player2Id) { // Generate a unique ID for the game room string roomId = databaseReference.Child("gameRooms").Push().Key; // Create a data structure representing the game room Dictionary<string, object> roomData = new Dictionary<string, object>(); roomData["player1Id"] = player1Id; roomData["player2Id"] = player2Id; roomData["status"] = "active"; // You can set initial status to "active" or "waiting" or any other status you prefer // Set the data in the database under the generated room ID databaseReference.Child("gameRooms").Child(roomId).SetValueAsync(roomData) .ContinueWith(task => { if (task.IsCompleted && !task.IsFaulted && !task.IsCanceled) { Debug.Log("Game room created successfully!"); // Transition the matched players to the gameplay scene only if it hasn't been done before if (!sceneTransitioned) { sceneTransitioned = true; SceneManager.LoadScene("GamePlay"); } } else if (task.IsFaulted) { // Handle the error foreach (Exception exception in task.Exception.InnerExceptions) { Debug.LogError("Failed to create game room: " + exception.Message); } } else if (task.IsCanceled) { Debug.LogWarning("Creating game room task was canceled."); } else { Debug.LogError("Unknown error occurred while creating game room."); } }); } }
核心问题分析
- 匹配逻辑触发单一:只有主动发起匹配的Player2会执行
FindMatch查询,Player1加入队列后没有持续监听队列状态,完全不知道已经凑齐两人,自然不会触发场景跳转。 - 场景跳转仅在发起查询的客户端执行:当前只有执行
CreateGameRoom的Player2会加载场景,Player1没有监听游戏房间的创建事件,无法触发跳转。 - 未清理匹配队列:创建房间后没有把两个玩家从
matchmaking节点移除,可能导致后续匹配逻辑混乱。
修复步骤
1. 改用实时监听匹配队列,替代单次查询
让每个玩家实时监听队列变化,一旦凑齐两人就触发匹配:
private void StartListeningForMatch() { databaseReference.Child("matchmaking").ValueChanged += HandleMatchmakingUpdate; } private void HandleMatchmakingUpdate(object sender, ValueChangedEventArgs args) { if (args.DatabaseError != null) { Debug.LogError(args.DatabaseError.Message); return; } DataSnapshot snapshot = args.Snapshot; if (snapshot != null && snapshot.HasChildren && snapshot.ChildrenCount >= 2) { // 先取消监听,避免重复触发 databaseReference.Child("matchmaking").ValueChanged -= HandleMatchmakingUpdate; List<string> playerIds = new List<string>(); foreach (DataSnapshot playerSnapshot in snapshot.Children) { string playerId = playerSnapshot.Key; playerIds.Add(playerId); } // 只让第一个加入队列的玩家创建房间,避免重复创建 if (playerIds[0] == currentUser.UserId) { CreateGameRoom(playerIds[0], playerIds[1]); } } }
修改StartMatchmaking,替换原来的FindMatch调用:
public void StartMatchmaking() { if (!isMatchmaking) { databaseReference.Child("matchmaking").Child(currentUser.UserId).SetValueAsync(true).ContinueWith(task => { if (task.IsCompleted && !task.IsFaulted && !task.IsCanceled) { isMatchmaking = true; // 开始监听队列变化 StartListeningForMatch(); } else { Debug.LogError("Failed to start matchmaking: " + task.Exception); } }); } else { Debug.LogWarning("User is already in matchmaking."); } }
2. 让所有匹配成功的玩家监听房间创建事件
每个玩家需要监听包含自己ID的房间,一旦房间创建就跳转场景:
private void StartListeningForGameRoom() { // 监听包含当前玩家ID的房间 databaseReference.Child("gameRooms").OrderByChild("player1Id").EqualTo(currentUser.UserId) .ValueChanged += HandleGameRoomUpdate; databaseReference.Child("gameRooms").OrderByChild("player2Id").EqualTo(currentUser.UserId) .ValueChanged += HandleGameRoomUpdate; } private void HandleGameRoomUpdate(object sender, ValueChangedEventArgs args) { if (args.DatabaseError != null) { Debug.LogError(args.DatabaseError.Message); return; } DataSnapshot snapshot = args.Snapshot; if (snapshot != null && snapshot.Exists) { // 取消所有监听 databaseReference.Child("matchmaking").ValueChanged -= HandleMatchmakingUpdate; databaseReference.Child("gameRooms").OrderByChild("player1Id").EqualTo(currentUser.UserId) .ValueChanged -= HandleGameRoomUpdate; databaseReference.Child("gameRooms").OrderByChild("player2Id").EqualTo(currentUser.UserId) .ValueChanged -= HandleGameRoomUpdate; // 清理匹配队列中的自己 databaseReference.Child("matchmaking").Child(currentUser.UserId).RemoveValueAsync(); // 跳转场景 if (!sceneTransitioned) { sceneTransitioned = true; SceneManager.LoadScene("GamePlay"); } } }
在StartMatchmaking中添加启动房间监听的代码:
// 在StartListeningForMatch();后面添加 StartListeningForGameRoom();
3. 创建房间后批量清理匹配队列
修改CreateGameRoom方法,用批量操作确保原子性:
private void CreateGameRoom(string player1Id, string player2Id) { string roomId = databaseReference.Child("gameRooms").Push().Key; Dictionary<string, object> roomData = new Dictionary<string, object>(); roomData["player1Id"] = player1Id; roomData["player2Id"] = player2Id; roomData["status"] = "active"; // 批量操作:创建房间 + 移除匹配队列中的玩家 Dictionary<string, object> updates = new Dictionary<string, object>(); updates["/gameRooms/" + roomId] = roomData; updates["/matchmaking/" + player1Id] = null; updates["/matchmaking/" + player2Id] = null; databaseReference.UpdateChildrenAsync(updates).ContinueWith(task => { if (task.IsCompleted && !task.IsFaulted && !task.IsCanceled) { Debug.Log("Game room created successfully!"); } else if (task.IsFaulted) { foreach (Exception exception in task.Exception.InnerExceptions) { Debug.LogError("Failed to create game room: " + exception.Message); } } else if (task.IsCanceled) { Debug.LogWarning("Creating game room task was canceled."); } else { Debug.LogError("Unknown error occurred while creating game room."); } }); }
后续功能建议
- 玩家名称同步:在数据库中创建
users节点存储每个玩家的ID、名称等信息,创建房间时同步到房间数据中,游戏场景中读取显示。 - 分数同步:在游戏房间节点下添加
player1Score和player2Score字段,游戏过程中实时更新,双方客户端监听字段变化同步分数显示。 - 匹配超时处理:给匹配队列添加超时逻辑,超过一定时间未匹配到玩家就自动退出队列并提示玩家。
内容的提问来源于stack exchange,提问作者taher
相关产品推荐
相关产品推荐

