You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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.");
                }
            });
    }
}

核心问题分析

  1. 匹配逻辑触发单一:只有主动发起匹配的Player2会执行FindMatch查询,Player1加入队列后没有持续监听队列状态,完全不知道已经凑齐两人,自然不会触发场景跳转。
  2. 场景跳转仅在发起查询的客户端执行:当前只有执行CreateGameRoom的Player2会加载场景,Player1没有监听游戏房间的创建事件,无法触发跳转。
  3. 未清理匹配队列:创建房间后没有把两个玩家从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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 03:15:05