1 概述

Firestore 和 Cloud Functions简介 中介绍了 Firestore 和 Cloud Functions 的基本用法,基于这两个系统,我们可以实现公会系统。

该公会系统允许在游戏或应用程序中创建、管理和互动公会。它使用 Firebase Firestore 作为数据存储和实时更新,确保了操作的高效和可扩展性。

2 基本数据结构

2.1 TeamInfo

TeamInfo.cs 文件详细定义了公会信息的数据结构,用于在 Firestore 数据库中存储和管理公会的基本信息。以下是对其主要内容的分析:

  • 使用 [FirestoreData] 属性标记类,使其与 Firestore 文档对应。
  • 包含多个 [FirestoreProperty] 属性,如 TeamName(公会名称)、MemberNum(成员数量)、IconIndex(图标索引)、TeamDescription(公会描述)、TeamScore(公会得分)、TeamId(公会 ID)、TeamRankSerialNumber(公会战序列号)、IsAdvancedGroup(是否为高级群组,公会战做区分)、IsOpen(是否公开)、JoinInLevel(加入等级)、CoLeaderNum(副领导数量)和 Random(随机数,用于获取随机序列)。这些属性反映了公会的基本信息和状态。
  • 提供了两个构造函数:一个是无参数构造函数,用于创建新的 TeamInfo 实例并初始化一些默认值;另一个接受 IDictionary 参数,用于从 Firestore 文档或其他字典类型数据中创建 TeamInfo 实例。

这个类的设计允许灵活地处理公会信息的存储和检索,支持公会创建、成员管理和公会信息更新等功能。通过 Firestore 的实时数据库特性,公会信息的任何更改都可以快速同步到所有客户端,为用户提供即时更新的体验。

2.2 TeamMemberInfo

TeamMemberInfo.cs 定义了公会系统中成员的详细信息结构。它使用 Firestore 数据模型,包含成员的名称、等级、帮助次数、唯一标识符(UUID)、领导状态、加入时间、是否激活了通行证、是否被禁言、最后更新时间、最高排名分数和头像索引等属性。此类为公会成员提供了一个全面的数据结构,以支持在应用中对成员的各种操作和状态管理。

通过两种构造函数,TeamMemberInfo 可以直接从游戏管理器中获取玩家数据来初始化,或者通过从 Firestore 文档或其他字典类型数据中提取信息来初始化。这种设计使得在实际应用中,无论是新成员加入还是现有成员信息的更新,都能够灵活高效地处理。

2.3 TeamMessage

TeamMessage 内置了很多 MessageType,根据 MessageType 不同,TeamMessage 所用到的字段也不同。

一些通用的字段如下:

  • RequesterUUID:发送这条消息的用户 UUID
  • RequesterName:发送这条消息的用户昵称
  • MessageID:消息 ID
  • IsAccess:消息是否被关闭
  • Timestamp:时间戳
  • MessageType:消息类型
  • IsRoyalPassActivated:发送消息的用户是否买了通行证
  1. 生命请求
  • SenderUUIDs:赠送生命的用户 UUID 列表,防止一个人赠送多次
  • SenderNames:赠送生命的用户昵称列表,收到生命的人本地需要赠送生命的人的名字
  • CurrentLifeNum:当前赠送生命数量
  • TotalLifeNum:总计生命数量 通过 FreeLifeStringPrasing 类来管理赠送过来的生命,当监听到自己发送的生命请求被赠送了生命时,就会触发 FreeLifeStringPrasing.Instance.AchieveFreeLife 给自己添加生命,然后调用 ClearSenderNames 将 SenderNames 清空。当最后一个人赠送完生命后,会将 IsAccess 置成 false,该消息在消息列表隐藏。
  1. 加入通知:无特殊字段
  2. 退出通知:无特殊字段
  3. 踢出通知:无特殊字段
  4. 加入请求:未实现
  5. 加入请求批准通知:未实现
  6. 皇家通行证通知 本地维护了一个 messageIDList,防止重复领取。
  7. 公会赛礼包通知
  • TeamRankPackageType:公会赛礼包种类
  1. 聊天消息
  • MessageContent:聊天内容

3 功能和接口

TeamComponent.cs 文件是公会系统的主要脚本,直接挂载在 GameManager.cs 下,其提供了大部分公会系统所用的接口,还有部分后续的补充接口写在了 TeamExtension.cs 下。

3.1 公会系统初始化

根据 TeamComponent.cs 文件的初始化部分,组件的初始化流程可以详细分析如下:

  1. 初始化 FirebaseFirestore 实例:通过 FirebaseFirestore.DefaultInstance 获取默认 Firestore 实例,用于后续的数据库操作。
  2. 初始化各种数据结构:包括 searchTeamInfosrandomTeamInfosteamMessageListchatMessageListmyTeamMessageListunHelpedMessageList 等,这些数据结构用于存储公会信息、消息等。
  3. 初始化 TeamMessageQueryManager:用于管理公会消息查询。
  4. 初始化公会和成员信息:创建 myMemberInfomyTeam 实例,用于存储当前用户的公会成员信息和公会信息。
  5. 设置默认网络 URL:通过 GameManager.Network.SetDefaultUrl 设置网络请求的默认 URL,该 URL 主要用于判断是否能连接上谷歌数据库。
  6. 初始化状态标志:包括 teamInfoInitedmyTeamInfoGotFromFirestore 等标志,用于跟踪公会信息的初始化状态。
  7. 获取公会 ID 并初始化公会信息:通过 GameManager.DataSave.GetTeamIdFromDatabase 异步获取公会 ID,然后调用 InitTeamInfo 方法进一步初始化公会信息,InitTeamInfo 会判断用户等级是否符合要求以及用户是否登录、公会 ID 是否为空等前置条件,然后执行不同的初始化操作:
    1. 如果用户未登录且用户等级不满足要求,则直接 return
    2. 如果用户已经加入了一个公会(即公会 ID 存在),则执行 GetTeamInfo 来初始化该用户的公会信息,包括从 Firestore 获取公会详细信息、成员列表等,并设置相关的状态标志,表示公会信息已经初始化完成。
    3. 如果用户没有加入任何公会(即公会 ID 不存在或为空),则删除本地公会信息并且拉取公会列表。
  8. 设置初始化完成标志:将 isInit 标志设置为 true,表示组件已完成初始化。

3.2 公会列表管理

为了保证用户拉取的公会列表是随机列表,每个公会信息都设置了一个 Random 字段作为随机种子,每当公会信息(成员数量、成员分数、公会简介)变更时,会同时更改 Random 的值,范围 [0,100)。获取用户列表主要通过 GetTeamInfoList 实现,该函数的具体实现如下:

  • num:需要获取的公会数量。
  • randomNum:用作查询条件的随机数。
  • call:完成操作时的可选回调动作。
  • isGreater:一个布尔值,表示是否只获取随机数大于randomNum的公会信息,默认为 True,当它为 False 时,表示第一次查询公会数量不足所需要的数量。
/// <summary>
/// 获取公会信息列表。
/// </summary>
/// <param name="num">需要获取的公会数量。</param>
/// <param name="randomNum">随机数,用于查询。</param>
/// <param name="call">完成操作时要调用的可选回调动作。</param>
/// <param name="isGreater">是否获取大于随机数的公会。</param>
private void GetTeamInfoList(int num, float randomNum, Action call = null, bool isGreater = true)
{
    Query teamQuery;
    // 根据isGreater的值,设置查询条件
    if (isGreater)
    {
        teamQuery = db.Collection($"/{databaseName}/TeamList/Teams")
            .WhereGreaterThan("Random", randomNum)
            .WhereEqualTo("IsOpen", true)
            .OrderBy("Random").Limit(num);
    }
    else
    {
        teamQuery = db.Collection($"/{databaseName}/TeamList/Teams")
            .WhereLessThanOrEqualTo("Random", randomNum)
            .WhereEqualTo("IsOpen", true)
            .OrderBy("Random").Limit(num);
    }
 
    // 异步获取查询结果
    teamQuery.GetSnapshotAsync().ContinueWithOnMainThread((getTask) =>
    {
        // 如果任务失败或被取消,调用回调函数并返回
        if (getTask.IsFaulted || getTask.IsCanceled)
        {
            call?.Invoke();
        }
        else
        {
            // 如果isGreater为真,清空randomTeamInfos
            if (isGreater)
            {
                if (randomTeamInfos == null)
                {
                    randomTeamInfos = new Dictionary<string, TeamInfo>();
                }
 
                randomTeamInfos.Clear();
            }
 
            // 遍历查询结果,将公会信息添加到randomTeamInfos
            foreach (var doc in getTask.Result.Documents)
            {
                var teamInfo = doc.ConvertTo<TeamInfo>();
                randomTeamInfos[teamInfo.TeamId] = teamInfo;
            }
 
            // 如果获取的公会数量小于需要的数量,再次调用GetTeamInfoList获取剩余的公会信息
            if (randomTeamInfos.Count < num && randomNum != 0)
            {
                GetTeamInfoList(num - randomTeamInfos.Count, randomNum, call, false);
            }
            else
            {
                // 否则,将randomTeamInfos转换为Json字符串并保存,设置获取公会列表的日期,设置正在获取公会列表的标志为false,调用回调函数,触发公会列表信息获取事件
                string teamListJson = SerializeTools.DicToJson(randomTeamInfos);
                GameManager.PlayerData.SetString(Constant.PlayerData.TeamInfoListJson, teamListJson);
                GameManager.PlayerData.SetDateTime(Constant.PlayerData.MergeTeamListDate, DateTime.Today);
                GameManager.DataNode.SetData("IsGettingTeamList", false);
                call?.Invoke();
                call = null;
                GameManager.Event.Fire(this, TeamListInfoGotEventArgs.Create(true));
            }
        }
    });
}

考虑到 Firestore 计费是通过读写次数计费,为了节约数据库费用,这里没有设置一个公会信息列表的监听器,并且限制了公会列表的拉取频率。该策略导致用户客户端的数据不是最新的,但是数据库访问量得到了显著降低。修改后的获取公会列表的实现如下:

/// <summary>
/// 检查并加载公会信息列表。
/// </summary>
/// <param name="callback">完成操作时要调用的可选回调动作。</param>
public void CheckAndLoadTeamInfoList(Action callback = null)
{
    // 如果玩家的当前等级低于开放公会所需的等级,
    // 或者玩家没有登录,或者玩家已经有一个公会,调用回调并返回。
    if (GameManager.PlayerData.NowLevel < GameManager.Firebase.GetLong(Constant.RemoteConfig.Openlevel_Team, 21)
        || GameManager.PlayerData.LoginType == 0 || GameManager.PlayerData.HasKey(Constant.PlayerData.MyTeamID))
    {
        callback?.Invoke();
        return;
    }
 
    // 如果正在获取公会列表,调用回调并返回。
    if (GameManager.DataNode.GetData("IsGettingTeamList", false))
    {
        callback?.Invoke();
        return;
    }
 
    // 设置正在获取公会列表的标志。
    GameManager.DataNode.SetData("IsGettingTeamList", true);
 
    // 获取上次合并公会列表的日期。
    DateTime time =
        GameManager.PlayerData.GetDateTime(Constant.PlayerData.MergeTeamListDate, Constant.GameConfig.DateTimeMin);
 
    // 如果当前日期在上次合并日期之后的3天以上,获取公会信息列表。
    if (DateTime.Now > time.AddDays(3))
    {
        float randomNum = Random.Range(0f, 100f);
        GetTeamInfoList(20, randomNum, callback);
    }
    else
    {
        // 否则,从玩家的数据中获取公会信息列表。
        string json = GameManager.PlayerData.GetString(Constant.PlayerData.TeamInfoListJson, "");
        randomTeamInfos = SerializeTools.DicFromJson<string, TeamInfo>(json);
 
        // 如果公会信息列表为空或者少于20个公会,获取公会信息列表。
        if (randomTeamInfos == null || randomTeamInfos.Count < 20)
        {
            randomTeamInfos = new Dictionary<string, TeamInfo>();
            float randomNum = Random.Range(0f, 100f);
            GetTeamInfoList(20, randomNum, callback);
        }
        else
        {
            // 否则,设置公会列表不在获取中的标志,
            // 触发已获取公会列表信息的事件,并调用回调。
            GameManager.DataNode.SetData("IsGettingTeamList", false);
            GameManager.Event.Fire(this, TeamListInfoGotEventArgs.Create(true));
            callback?.Invoke();
        }
    }
}

3.3 公会管理

  • 公会信息查询:用户查询特定公会的信息。
/// <summary>
/// 获取指定公会的信息。
/// </summary>
/// <param name="teamId">公会的ID。</param>
/// <param name="teamAction">获取公会信息后的回调函数。</param>
public void GetTeamInfo(string teamId, Action<TeamInfo> teamAction)
{
    // 如果玩家未登录,直接返回。
    if (GameManager.PlayerData.LoginType == 0)
    {
        teamAction?.Invoke(null);
        return;
    }
 
    // 获取玩家的UUID。
    string uuid = GameManager.PlayerData.GetString(Constant.PlayerData.UserUID, "");
    // 如果UUID为空,直接返回。
    if (string.IsNullOrEmpty(uuid))
    {
        teamAction?.Invoke(null);
        return;
    }
 
    // 获取指定公会的文档引用。
    DocumentReference teamRef = db.Collection($"/{databaseName}/TeamList/Teams").Document(teamId);
    TeamInfo teamInfo = null;
    // 异步获取公会信息。
    teamRef.GetSnapshotAsync().ContinueWithOnMainThread((getTask) =>
    {
        // 如果任务被取消或失败,记录警告并调用回调函数。
        if (getTask.IsCanceled || getTask.IsFaulted)
        {
            Log.Warning("Get TeamInfo Failed.Exception:{0}",
                getTask.Exception != null ? getTask.Exception.ToString() : string.Empty);
            teamAction?.Invoke(teamInfo);
        }
        else
        {
            // 如果任务成功,将获取的文档转换为公会信息,并调用回调函数。
            var snapshot = getTask.Result;
            teamInfo = snapshot.ConvertTo<TeamInfo>();
            teamAction?.Invoke(teamInfo);
        }
    });
}

同时获取公会和成员信息:

/// <summary>
/// 获取指定公会的信息和成员列表。
/// </summary>
/// <param name="teamId">公会的ID。</param>
/// <param name="teamAction">获取公会信息后的回调函数。</param>
public void GetTeamInfoAndMembers(string teamId, Action<TeamInfo> teamAction)
{
    // 如果玩家未登录,直接返回。
    if (GameManager.PlayerData.LoginType == 0)
    {
        teamAction?.Invoke(null);
        return;
    }
 
    // 获取玩家的UUID。
    string uuid = GameManager.PlayerData.GetString(Constant.PlayerData.UserUID, "");
    // 获取指定公会的文档引用。
    DocumentReference teamRef = db.Collection($"/{databaseName}/TeamList/Teams").Document(teamId);
    TeamInfo teamInfo = null;
    // 异步获取公会信息。
    teamRef.GetSnapshotAsync().ContinueWithOnMainThread((getTask) =>
    {
        // 如果任务被取消或失败,记录警告并调用回调函数。
        if (getTask.IsCanceled || getTask.IsFaulted)
        {
            Log.Warning("Get TeamInfo Failed.Exception:{0}",
                getTask.Exception != null ? getTask.Exception.ToString() : string.Empty);
            teamAction?.Invoke(teamInfo);
        }
        else
        {
            // 如果任务成功,将获取的文档转换为公会信息,并调用回调函数。
            var snapshot = getTask.Result;
            teamInfo = snapshot.ConvertTo<TeamInfo>();
            if (teamInfo == null)
            {
                teamAction?.Invoke(null);
                return;
            }
 
            // 如果获取的公会ID与我的公会ID相同,更新我的公会信息。
            if (teamInfo.TeamId == myTeam.TeamId)
            {
                myTeam = teamInfo;
                GameManager.PlayerData.SetString(Constant.PlayerData.MyTeamName, myTeam.TeamName);
                GameManager.PlayerData.SetInt(Constant.PlayerData.MyTeamIconID, myTeam.IconIndex);
                GameManager.PlayerData.SetString(Constant.PlayerData.MyTeamID, myTeam.TeamId);
            }
 
            // 获取公会成员列表的引用。
            CollectionReference teamMembersRef = teamRef.Collection("MemberList");
            // 按照"CurrentLevel"字段排序并异步获取成员列表。
            teamMembersRef.OrderBy("CurrentLevel").GetSnapshotAsync().ContinueWithOnMainThread((getMemberTask) =>
            {
                // 如果任务被取消或失败,记录警告并调用回调函数。
                if (getMemberTask.IsCanceled || getMemberTask.IsFaulted)
                {
                    Log.Warning("Get MemberList Failed");
                    teamAction?.Invoke(teamInfo);
                }
                else
                {
                    // 如果任务成功,遍历查询结果,将成员信息添加到公会信息中,并调用回调函数。
                    var querySnapShot = getMemberTask.Result;
                    foreach (DocumentSnapshot documentSnapshot in querySnapShot.Documents)
                    {
                        TeamMemberInfo member = documentSnapshot.ConvertTo<TeamMemberInfo>();
                        if (!documentSnapshot.ContainsField("IsRoyalPassActivated"))
                        {
                            member.IsRoyalPassActivated = false;
                        }
 
                        // 如果成员的UUID与我的UUID相同,更新我的成员信息。
                        if (member.UUID == myMemberInfo.UUID)
                        {
                            myMemberInfo.IsCoLeader = member.IsCoLeader;
                            GameManager.PlayerData.SetBool(Constant.PlayerData.IsCoLeader, member.IsCoLeader);
                            myMemberInfo.IsLeader = member.IsLeader;
                            GameManager.PlayerData.SetBool(Constant.PlayerData.IsLeader, member.IsLeader);
                            myMemberInfo.JoinInTime = member.JoinInTime;
                            GameManager.PlayerData.SetDateTime(Constant.PlayerData.JoinInTimestamp,
                                member.JoinInTime.ToDateTime().ToLocalTime());
                            myMemberInfo.HelpNum = member.HelpNum;
                            myMemberInfo.IsMute = member.IsMute;
                            GameManager.PlayerData.SetBool(Constant.PlayerData.IsMute, member.IsMute);
                            myMemberInfo.PeekRankScore =
                                GameManager.PlayerData.GetBool(Constant.PlayerData.PeekRankInited)
                                    ? GameManager.PlayerData.GetInt(Constant.PlayerData.PeekRankScore)
                                    : 0;
                            member.PeekRankScore = myMemberInfo.PeekRankScore;
                        }
 
                        teamInfo.TeamMembers.Add(member);
                    }
 
                    teamAction?.Invoke(teamInfo);
                }
            });
        }
    });
}
  • 加入公会:用户能够搜索加入现有公开公会。
/// <summary>
/// 使用公会ID加入一个公会
/// </summary>
/// <param name="teamId">要加入的公会的ID</param>
/// <param name="isFinished">加入公会操作完成后的回调函数</param>
/// <param name="isRobot">是否为机器人,默认为false</param>
public void JoinATeamWithTeamID(string teamId, Action<bool> isFinished = null, bool isRobot = false)
{
    // 如果玩家未登录,直接返回
    if (GameManager.PlayerData.LoginType == 0)
    {
        isFinished?.Invoke(false);
        return;
    }
 
    // 获取玩家的UUID
    string uuid = GameManager.PlayerData.GetString(Constant.PlayerData.UserUID, "");
 
    // 获取指定公会的文档引用
    DocumentReference documentReference = db.Collection($"/{databaseName}/TeamList/Teams").Document(teamId);
    // 异步获取公会信息
    documentReference.GetSnapshotAsync().ContinueWithOnMainThread((getTask) =>
    {
        // 如果任务失败或被取消,记录警告并调用回调函数
        if (getTask.IsFaulted || getTask.IsCanceled)
        {
            Log.Warning("加入公会失败");
            isFinished?.Invoke(false);
        }
        else
        {
            // 如果任务成功,将获取的文档转换为公会信息
            myTeam = getTask.Result.ConvertTo<TeamInfo>();
            // 如果公会信息为空,调用回调函数并返回
            if (myTeam == null)
            {
                myTeam = new TeamInfo();
                isFinished?.Invoke(false);
                return;
            }
 
            // 更新我的公会信息,并保存到玩家数据中
            myTeamInfoGotFromFirestore = true;
            GameManager.PlayerData.SetString(Constant.PlayerData.MyTeamName, myTeam.TeamName);
            GameManager.PlayerData.SetInt(Constant.PlayerData.MyTeamIconID, myTeam.IconIndex);
            GameManager.PlayerData.SetString(Constant.PlayerData.MyTeamID, teamId);
            Log.Info(GameManager.PlayerData.GetString(Constant.PlayerData.MyTeamID));
            // 获取当前时间戳,并保存到玩家数据中
            Timestamp timestamp = Timestamp.GetCurrentTimestamp();
            GameManager.PlayerData.SetDateTime(Constant.PlayerData.JoinInTimestamp,
                timestamp.ToDateTime().ToLocalTime());
            // 创建一个新的公会成员信息,并设置加入时间
            myMemberInfo = new TeamMemberInfo();
            myMemberInfo.JoinInTime = timestamp;
            // 检查并保存当前等级
            GameManager.DataSave.CheckCurrentLevelAndSave();
            // 触发有公会状态改变的事件
            GameManager.Event.Fire(this, HasTeamStatusChangedEventArgs.Create(true));
            Log.Info("HasTeamStatusChangedEventArgs Count " +
                     GameManager.Event.Count(HasTeamStatusChangedEventArgs.EventId));
            // 触发我的公会信息获取的事件
            GameManager.Event.Fire(this, MyTeamInfoGotEventArgs.Create());
            // 发送加入通知
            SendJoinInNotify(teamId);
            // 设置监听器
            SetListeners();
            // 更新我的数据到公会
            UpdateMyDataToTeamAsync(myMemberInfo, true, isFinished);
        }
    });
}
  • 创建公会:允许用户创建新公会,生成唯一 ID 并初始化公会属性。
/// <summary>
/// 创建一个新的公会
/// </summary>
/// <param name="myTeamInfo">新公会的信息</param>
/// <param name="isFinished">创建公会操作完成后的回调函数</param>
public void CreateATeam(TeamInfo myTeamInfo, Action<bool> isFinished = null)
{
    // 生成一个新的GUID作为公会ID
    Guid guid = Guid.NewGuid();
    myTeamInfo.TeamId = guid.ToString();
    // 设置公会的随机数为-1
    myTeamInfo.Random = -1;
    // 设置公会的成员数量为1
    myTeamInfo.MemberNum = 1;
    // 获取新公会的文档引用
    DocumentReference myTeamRef = db.Collection($"/{databaseName}/TeamList/Teams").Document(myTeamInfo.TeamId);
 
    // 异步设置新公会的信息
    myTeamRef.SetAsync(myTeamInfo, SetOptions.MergeAll).ContinueWithOnMainThread((task) =>
    {
        // 如果任务被取消或失败,记录警告并调用回调函数
        if (task.IsCanceled || task.IsFaulted)
        {
            Log.Warning("CreateFailed");
            isFinished?.Invoke(false);
        }
        else if (task.IsCompleted)
        {
            // 如果任务成功,设置玩家为公会领导,保存公会ID,更新我的公会信息,并加入新公会
            GameManager.PlayerData.SetBool(Constant.PlayerData.IsLeader, true);
            GameManager.PlayerData.SetString(Constant.PlayerData.MyTeamID, myTeamInfo.TeamId);
            myTeam = myTeamInfo;
            JoinATeamWithTeamID(guid.ToString(), isFinished);
        }
    });
}
  • 离开公会:允许成员离开公会,将他们的数据从公会成员列表中移除。
/// <summary>
/// 离开当前公会
/// </summary>
/// <param name="callback">离开公会操作完成后的回调函数</param>
public void LeaveCurrentTeam(Action callback = null)
{
    // 记录离开公会的信息
    Log.Info("LeaveCurrentTeam" + GameManager.PlayerData.GetString(Constant.PlayerData.MyTeamID));
    // 检查玩家是否有公会ID
    var hasTeamId = GameManager.PlayerData.HasKey(Constant.PlayerData.MyTeamID);
    string teamId = myTeam.TeamId;
    Log.Info("LeaveCurrentTeam HasTeamID" + hasTeamId);
 
    // 如果玩家未登录或没有公会ID,直接返回
    if (GameManager.PlayerData.LoginType == 0 || !hasTeamId)
    {
        callback?.Invoke();
        return;
    }
 
    // 获取当前公会的文档引用
    DocumentReference myTeamRef = db.Collection($"/{databaseName}/TeamList/Teams").Document(myTeam.TeamId);
    // 获取当前成员的文档引用
    DocumentReference myMemberRef = db.Collection($"/{databaseName}/TeamList/Teams/{myTeam.TeamId}/MemberList")
        .Document(myMemberInfo.UUID);
 
    // 异步删除当前成员的文档
    myMemberRef.DeleteAsync().ContinueWithOnMainThread((deltask) =>
    {
        // 如果任务完成
        if (deltask.IsCompleted)
        {
            // 如果玩家是公会领导,更改公会领导
            if (GameManager.DataNode.GetData("IsTeamLeaderBefore", false))
            {
                ChangeLeader(myTeamRef);
                GameManager.DataNode.SetData("IsTeamLeaderBefore", false);
            }
 
            // 关闭所有我在退出公会时的消息
            CloseAllMyMessagesOnQuitTeam(teamId);
            // 创建一个离开通知的文档引用
            DocumentReference leaveMsgRef =
                db.Collection($"/{databaseName}/TeamList/Teams/{teamId}/MessageList").Document();
            // 创建一个离开通知的消息
            TeamMessage leaveMessage = new TeamMessage(TeamMessageType.QuitNotify);
            leaveMessage.MessageID = leaveMsgRef.Id;
            // 异步设置离开通知的消息
            leaveMsgRef.SetAsync(leaveMessage);
 
            // 记录离开公会后的公会信息列表的数量
            Log.Info("LeaveTeam" + randomTeamInfos.Count);
            // 检查并加载公会信息列表
            CheckAndLoadTeamInfoList(callback);
        }
    });
}
  • 更新公会信息:会长修改公会信息直接上传,公会成员数量以及总分数用 function 自动更新。
/// <summary>
/// 更新公会信息
/// </summary>
public void UpdateTeamInfo()
{
    // 如果玩家未登录或没有公会ID,直接返回
    if (GameManager.PlayerData.LoginType == 0 || !GameManager.PlayerData.HasKey(Constant.PlayerData.MyTeamID))
    {
        return;
    }
 
    // 获取当前公会的文档引用
    DocumentReference teamRef = db.Collection($"/{databaseName}/TeamList/Teams").Document(myTeam.TeamId);
    // 运行一个异步事务
    db.RunTransactionAsync(trans =>
    {
        // 获取公会的快照
        return teamRef.GetSnapshotAsync().ContinueWithOnMainThread(continuation =>
        {
            // 如果任务被取消或失败,返回false
            if (continuation.IsFaulted || continuation.IsCanceled)
            {
                return false;
            }
 
            // 如果任务成功,获取文档快照
            DocumentSnapshot snapshot = continuation.Result;
            // 将文档快照转换为公会信息
            TeamInfo team = snapshot.ConvertTo<TeamInfo>();
            // 更新我的公会成员数量
            myTeam.MemberNum = team.MemberNum;
            // 创建一个字典来存储要更新的数据
            Dictionary<string, object> data = new Dictionary<string, object>();
            // 遍历我的公会的每个属性,将属性名称和对应的值添加到数据字典中
            foreach (var item in myTeam.GetType().GetProperties())
            {
                data.Add(item.Name, item.GetValue(myTeam));
            }
 
            // 更新公会引用的数据
            trans.Update(teamRef, data);
            // 返回true表示任务成功
            return true;
        });
    });
}

3.4 成员管理

  • 移除成员:移除公会成员,直接移除数据库中的用户数据,并且关闭用户的 message,用户本地客户端 listener 监听到事件之后,自动执行退出操作。
/// <summary>
/// 通过UUID踢出公会成员
/// </summary>
/// <param name="teamComponent">公会组件</param>
/// <param name="memberInfo">要踢出的公会成员信息</param>
public static void KickMemberByUUID(this TeamComponent teamComponent, TeamMemberInfo memberInfo)
{
    // 如果当前用户既不是公会领导也不是副领导,则直接返回
    if (!teamComponent.myMemberInfo.IsCoLeader && !teamComponent.myMemberInfo.IsLeader)
    {
        return;
    }
 
    // 获取当前公会的文档引用
    DocumentReference myTeamRef = teamComponent.db.Collection("/Team/TeamList/Teams").Document(teamComponent.myTeam.TeamId);
 
    // 生成一个随机数
    float randomNum = Random.Range(0f, 100f);
 
    // 运行一个异步事务
    teamComponent.db.RunTransactionAsync((transaction) =>
    {
        // 获取公会的快照
        return transaction.GetSnapshotAsync(myTeamRef).ContinueWithOnMainThread((getTask) =>
        {
            // 如果任务完成
            if (getTask.IsCompleted)
            {
                // 获取文档快照
                DocumentSnapshot snapshot = getTask.Result;
 
                // 创建一个字典来存储要更新的数据
                Dictionary<string, object> updates = new Dictionary<string, object>
                {
                    { "Random", randomNum }
                };
 
                // 更新公会引用的数据
                transaction.Update(myTeamRef, updates);
 
                // 返回true表示任务成功
                return true;
            }
            else
            {
                // 返回false表示任务失败
                return false;
            }
        });
    }).ContinueWithOnMainThread((transactionTask)=>
    {
        // 如果事务成功
        if (transactionTask.Result)
        {
            // 获取当前成员的文档引用
            DocumentReference memberDoc = myTeamRef.Collection("MemberList").Document(memberInfo.UUID);
 
            // 异步删除当前成员的文档
            memberDoc.DeleteAsync().ContinueWithOnMainThread(task=>
            {
                // 触发公会成员列表改变的事件
                GameManager.Event.Fire(teamComponent, TeamMemberListChangedEventArgs.Create(teamComponent.myTeam.TeamId));
 
                // 发送踢出通知
                SendKickOutMessage(teamComponent, memberInfo);
 
                // 关闭所有该成员的生命请求和皇家通行证通知
                CloseAllSomeonesRequests(teamComponent, memberInfo.UUID,TeamMessageType.RequestLives);
                CloseAllSomeonesRequests(teamComponent, memberInfo.UUID,TeamMessageType.RoyalPassNotify);
 
                // 删除公会排名成员数据
                GameManager.Activity.TeamRankManager.DeleteTeamRankMemberData(memberInfo.UUID);
            });
 
            // 通过UUID设置Firestore公会ID
            SetFirestoreTeamIDByUUID(teamComponent, memberInfo.UUID);
        }
        else
        {
            // 触发公会成员列表改变的事件
            GameManager.Event.Fire(teamComponent, TeamMemberListChangedEventArgs.Create(teamComponent.myTeam.TeamId));
        }
    });
}
// 监听自己的公会信息
public void SetMyMemberInfoListener()
{
    if (GameManager.PlayerData.LoginType == 0 || !GameManager.PlayerData.HasKey(Constant.PlayerData.MyTeamID))
    {
        return;
    }
    myTeam.TeamId = GameManager.PlayerData.GetString(Constant.PlayerData.MyTeamID);
    CollectionReference memberCollectionRef = db.Collection($"/{databaseName}/TeamList/Teams/{myTeam.TeamId}/MemberList");
    Query query = memberCollectionRef.WhereEqualTo("UUID", myMemberInfo.UUID);
    if (MyMemberInfoListener != null)
    {
        MyMemberInfoListener.Stop();
        MyMemberInfoListener.Dispose();
    }
    MyMemberInfoListener = query.Listen((callback) =>
    {
        foreach (var change in callback.GetChanges())
        {
            if (change.ChangeType == DocumentChange.Type.Modified)
            {
                var doc = change.Document.ConvertTo<TeamMemberInfo>();
                myMemberInfo = doc;
                if (GameManager.PlayerData.GetBool(Constant.PlayerData.IsMute) != doc.IsMute)
                {
                    GameManager.PlayerData.SetBool(Constant.PlayerData.IsMute, doc.IsMute);
                    GameManager.Event.Fire(this, TeamMemberMuteChangedEventArgs.Create(doc.IsMute));
                }
            }
            //被踢了
            else if (change.ChangeType == DocumentChange.Type.Removed)
            {
                if (myMemberInfo.IsLeader)
                {
                    GameManager.DataNode.SetData("IsTeamLeaderBefore", true);
                }
                DeleteTeamKeys();
            }
        }
    });
}
//用户上线后先检查自己是否被踢了
public void CheckIfKicked(Action<bool> callback)
{
    if (GameManager.PlayerData.LoginType == 0 || !GameManager.PlayerData.HasKey(Constant.PlayerData.MyTeamID))
    {
        callback?.Invoke(true);
        return;
    }
    myTeam.TeamId = GameManager.PlayerData.GetString(Constant.PlayerData.MyTeamID);
    CollectionReference myDocRef = db.Collection($"/{databaseName}/TeamList/Teams/{myTeam.TeamId}/MemberList");
    Query query = myDocRef.WhereEqualTo("UUID", myMemberInfo.UUID);
    query.GetSnapshotAsync().ContinueWithOnMainThread((task) =>
    {
        if (task.IsFaulted || task.IsCanceled)
        {
            Log.Warning("CheckTask is Canceled or Faulted");
            callback?.Invoke(true);
        }
        else if (task.IsCompleted)
        {
            if (task.Result.Count > 0)
            {
                callback?.Invoke(false);
            }
            else
            {
                callback?.Invoke(true);
            }
        }
    });
}

3.5 消息和通知

目前允许在公会内发送的消息类型都写在了 TeamMessageType 中,其中包括了生命请求、加入通知、退出通知、踢人通知、通行证消息、公会战礼包消息、聊天消息,其中加入通知、退出通知、踢人通知等只有会长能看见。消息的实时发送与接受用的是 Firestore 的监听器系统,每一个成员加入公会时都会设置好监听器,当出现消息更改或者公会信息更改时,监听器会自动获取相应的消息并且在客户端做对应的操作,下面是一些监听器的例子:

/// <summary>
/// 设置普通消息监听器
/// </summary>
public void SetMessageListener()
{
    // 如果玩家未登录或没有公会ID,直接返回
    if (GameManager.PlayerData.LoginType == 0 || !GameManager.PlayerData.HasKey(Constant.PlayerData.MyTeamID))
    {
        return;
    }
 
    // 获取公会ID
    myTeam.TeamId = GameManager.PlayerData.GetString(Constant.PlayerData.MyTeamID);
    // 获取消息集合的引用
    CollectionReference msgCollectionRef =
        db.Collection($"/{databaseName}/TeamList/Teams/{myTeam.TeamId}/MessageList");
    // 如果已经存在监听器,停止并销毁它
    if (AllMessageListener != null)
    {
        AllMessageListener.Stop();
        AllMessageListener.Dispose();
    }
 
    // 定义一个列表,包含之前不需要的消息类型
    List<object> unUsed = new List<object>() { 1, 2, 3, 4, 5 };
    // 创建查询,获取之前不需要的消息类型,按时间戳降序排序,最多获取20条
    Query query = msgCollectionRef
        .WhereIn("MessageType", unUsed.AsEnumerable())
        .OrderByDescending("Timestamp")
        .Limit(20);
 
    // 设置监听器,当查询结果发生变化时触发
    AllMessageListener = query.Listen((callback) =>
    {
        // 设置监听器状态为已设置
        listenerSet = true;
        // 如果没有获取到任何消息,检查网络连接
        if (callback.GetChanges().Count() == 0)
        {
            // 如果网络不可达,显示网络重试界面
            if (!GameManager.Network.IsNetworkReachable)
            {
                var uiForm = UIComponent.Instance.GetUIForm("TeamBaseMenu", "Area4");
                if (uiForm != null)
                {
                    uiForm.GetComponent<TeamBaseMenu>().ShowNetworkLoadingMenu();
                }
            }
        }
        else
        {
            // 如果获取到了消息,设置网络连接状态为已连接
            TeamNetworkConnection = true;
        }
 
        // 遍历所有的变化
        foreach (var change in callback.GetChanges().Reverse())
        {
            // 打印变化的类型
            Log.Info("Changed " + change.ChangeType.ToString());
 
            // 如果消息类型超出枚举的范围,跳过这条消息
            if (change.Document.GetValue<int>("MessageType") > (Enum.GetValues(typeof(TeamMessageType)).Length - 1))
            {
                continue;
            }
 
            // 根据变化的类型进行不同的操作
            if (change.ChangeType == DocumentChange.Type.Added)
            {
                // 如果是新增的消息,将其添加到消息列表中
                var doc = change.Document.ConvertTo<TeamMessage>();
                teamMessageList.Add(doc);
                // 如果消息是由当前用户请求的,将其添加到用户的消息列表中
                if (doc.RequesterUUID == GameManager.PlayerData.GetString(Constant.PlayerData.UserUID))
                {
                    myTeamMessageList.Add(doc);
                }
 
                // 触发新消息获取事件
                GameManager.Event.Fire(this, NewTeamMessageGotEventArgs.Create(doc));
            }
            else if (change.ChangeType == DocumentChange.Type.Modified)
            {
                // 如果是修改的消息,更新消息列表中的对应消息
                var doc = change.Document.ConvertTo<TeamMessage>();
                for (int i = 0; i < teamMessageList.Count; i++)
                {
                    var message = teamMessageList[i];
                    if (message.MessageID == doc.MessageID)
                    {
                        teamMessageList[i] = doc;
                    }
                }
 
                // 触发消息修改事件
                GameManager.Event.Fire(this,
                    TeamMessageModifiedEventArgs.Create(change.Document.ConvertTo<TeamMessage>()));
            }
            else if (change.ChangeType == DocumentChange.Type.Removed)
            {
                // 如果是删除的消息,从消息列表中移除对应的消息
                var doc = change.Document.ConvertTo<TeamMessage>();
                for (int i = 0; i < teamMessageList.Count; i++)
                {
                    var message = teamMessageList[i];
                    if (message.MessageID == doc.MessageID)
                    {
                        teamMessageList.RemoveAt(i);
                    }
                }
 
                // 触发消息移除事件
                GameManager.Event.Fire(this, RemoveTeamMessageEventArgs.Create(doc));
            }
        }
    });
}

下面是聊天的消息监听器:

/// <summary>
/// 设置聊天消息监听器
/// </summary>
public void SetChatMessageListeners()
{
    // 获取我的公会ID
    myTeam.TeamId = GameManager.PlayerData.GetString(Constant.PlayerData.MyTeamID);
    // 获取消息集合的引用
    CollectionReference msgCollectionRef =
        db.Collection($"/{databaseName}/TeamList/Teams/{myTeam.TeamId}/MessageList");
    // 如果已经存在监听器,停止并销毁它
    if (ChatMessageListener != null)
    {
        ChatMessageListener.Stop();
        ChatMessageListener.Dispose();
    }
 
    // 获取加入时间的前7天
    var joininTime = myMemberInfo.JoinInTime.ToDateTime().AddDays(-7);
    var timestamp = Timestamp.FromDateTime(joininTime);
    // 创建查询,获取最近7天的聊天消息,按时间戳降序排序,最多获取30条
    Query query = msgCollectionRef
        .WhereEqualTo("MessageType", 8)
        .OrderByDescending("Timestamp")
        .Limit(30);
 
    // 设置监听器,当查询结果发生变化时触发
    ChatMessageListener = query.Listen((callback) =>
    {
        // 设置监听器状态为已设置
        listenerSet = true;
        // 遍历所有的变化
        foreach (var change in callback.GetChanges().Reverse())
        {
            // 打印变化的类型
            Log.Info("Changed " + change.ChangeType.ToString());
 
            // 如果消息已被删除,跳过这条消息
            if (deletedMessageList.Contains(change.Document.ConvertTo<TeamMessage>().MessageID))
            {
                continue;
            }
 
            // 根据变化的类型进行不同的操作
            if (change.ChangeType == DocumentChange.Type.Added)
            {
                // 如果是新增的消息,将其添加到消息列表中
                var doc = change.Document.ConvertTo<TeamMessage>();
                teamMessageList.Add(doc);
                // 如果消息是由当前用户发送的,将其添加到用户的消息列表中
                if (doc.RequesterUUID == GameManager.PlayerData.GetString(Constant.PlayerData.UserUID))
                {
                    myTeamMessageList.Add(doc);
                }
 
                // 触发新消息获取事件
                GameManager.Event.Fire(this, NewTeamMessageGotEventArgs.Create(doc));
            }
            else if (change.ChangeType == DocumentChange.Type.Modified)
            {
                // 如果是修改的消息,更新消息列表中的对应消息
                var doc = change.Document.ConvertTo<TeamMessage>();
                for (int i = 0; i < teamMessageList.Count; i++)
                {
                    var message = teamMessageList[i];
                    if (message.MessageID == doc.MessageID)
                    {
                        teamMessageList[i] = doc;
                    }
                }
 
                // 触发消息修改事件
                GameManager.Event.Fire(this,
                    TeamMessageModifiedEventArgs.Create(change.Document.ConvertTo<TeamMessage>()));
            }
            else if (change.ChangeType == DocumentChange.Type.Removed)
            {
                // 如果是删除的消息,从消息列表中移除对应的消息
                var doc = change.Document.ConvertTo<TeamMessage>();
                for (int i = 0; i < teamMessageList.Count; i++)
                {
                    var message = teamMessageList[i];
                    if (message.MessageID == doc.MessageID)
                    {
                        teamMessageList.RemoveAt(i);
                    }
                }
 
                // 触发消息移除事件
                GameManager.Event.Fire(this, RemoveTeamMessageEventArgs.Create(doc));
            }
        }
    });
}

最后是生命请求的监听器:

/// <summary>
/// 设置生命请求消息监听器
/// </summary>
public void SetLifeMessageListeners()
{
    // 获取我的公会ID
    myTeam.TeamId = GameManager.PlayerData.GetString(Constant.PlayerData.MyTeamID);
    // 获取消息集合的引用
    CollectionReference msgCollectionRef =
        db.Collection($"/{databaseName}/TeamList/Teams/{myTeam.TeamId}/MessageList");
    // 如果已经存在监听器,停止并销毁它
    if (LifeMessageListener != null)
    {
        LifeMessageListener.Stop();
        LifeMessageListener.Dispose();
    }
 
    // 获取加入时间的前7天
    var joininTime = myMemberInfo.JoinInTime.ToDateTime().AddDays(-7);
    var timestamp = Timestamp.FromDateTime(joininTime);
    // 创建查询,获取最近7天的生命请求消息,按时间戳降序排序,最多获取20条
    Query query = msgCollectionRef
        .WhereEqualTo("MessageType", 0)
        .OrderByDescending("Timestamp")
        .Limit(20);
 
    // 设置监听器,当查询结果发生变化时触发
    LifeMessageListener = query.Listen((callback) =>
    {
        // 设置监听器状态为已设置
        listenerSet = true;
        // 遍历所有的变化
        foreach (var change in callback.GetChanges().Reverse())
        {
            // 打印变化的类型
            Log.Info("Changed " + change.ChangeType.ToString());
 
            // 如果消息类型超出枚举的范围,跳过这条消息
            if (change.Document.GetValue<int>("MessageType") > (Enum.GetValues(typeof(TeamMessageType)).Length - 1))
            {
                continue;
            }
 
            // 根据变化的类型进行不同的操作
            if (change.ChangeType == DocumentChange.Type.Added)
            {
                // 如果是新增的消息,将其添加到消息列表中
                var doc = change.Document.ConvertTo<TeamMessage>();
                teamMessageList.Add(doc);
                // 如果消息是由当前用户请求的,将其添加到用户的消息列表中
                if (doc.RequesterUUID == GameManager.PlayerData.GetString(Constant.PlayerData.UserUID))
                {
                    myTeamMessageList.Add(doc);
                    // 如果消息是生命请求,并且发送者名字列表不为空,添加生命
                    if (doc.MessageType == TeamMessageType.RequestLives && doc.SenderNames.Count > 0)
                    {
                        foreach (var providerName in doc.SenderNames)
                        {
                            FreeLifeStringPrasing.Instance.AchieveFreeLife(providerName);
                        }
 
                        doc.SenderNames.Clear();
                        ClearSenderNames(doc.MessageID);
                    }
                }
 
                // 如果不是自己请求的生命,并且消息是打开状态,且消息类型是生命请求
                List<string> messages = PlayerPrefsX.GetStringArray(Constant.PlayerData.MessageIDList).ToList();
                if (doc.RequesterUUID != myMemberInfo.UUID && doc.IsAccess &&
                    doc.MessageType == TeamMessageType.RequestLives)
                {
                    // 如果发送者UUID列表不包含我的UUID,并且消息ID列表不包含这条消息的ID
                    if (!doc.SenderUUIDs.Contains(myMemberInfo.UUID) && !messages.Contains(doc.MessageID))
                    {
                        // 将这条消息的ID添加到未帮助的消息列表中
                        unHelpedMessageList.Add(doc.MessageID);
                        // 触发生命请求数量改变事件
                        GameManager.Event.Fire(this,
                            LifeRequestNumChangedEventArgs.Create(unHelpedMessageList.Count));
                    }
                }
 
                // 触发新公会消息获取事件
                GameManager.Event.Fire(this, NewTeamMessageGotEventArgs.Create(doc));
            }
            // 如果是修改的消息,更新消息列表中的对应消息
            else if (change.ChangeType == DocumentChange.Type.Modified)
            {
                // 触发公会消息修改事件
                GameManager.Event.Fire(this,
                    TeamMessageModifiedEventArgs.Create(change.Document.ConvertTo<TeamMessage>()));
 
                var doc = change.Document.ConvertTo<TeamMessage>();
                // 如果未帮助的消息列表包含这条消息的ID,并且消息是关闭状态
                if (unHelpedMessageList.Contains(doc.MessageID) && !doc.IsAccess)
                {
                    // 从未帮助的消息列表中移除这条消息的ID
                    unHelpedMessageList.Remove(doc.MessageID);
                    // 触发生命请求数量改变事件
                    GameManager.Event.Fire(this, LifeRequestNumChangedEventArgs.Create(unHelpedMessageList.Count));
                }
 
                // 在公会消息列表中找到这条消息,并更新它
                for (int i = 0; i < teamMessageList.Count; i++)
                {
                    var message = teamMessageList[i];
                    if (message.MessageID == doc.MessageID)
                    {
                        teamMessageList[i] = doc;
                    }
                }
 
                // 将生命添加到免费生命中
                AddLifeToFreeLives(doc);
            }
            // 如果是删除的消息,从消息列表中移除对应的消息
            else if (change.ChangeType == DocumentChange.Type.Removed)
            {
                var doc = change.Document.ConvertTo<TeamMessage>();
                for (int i = 0; i < teamMessageList.Count; i++)
                {
                    var message = teamMessageList[i];
                    if (message.MessageID == doc.MessageID)
                    {
                        teamMessageList.RemoveAt(i);
                    }
                }
 
                // 触发消息移除事件
                GameManager.Event.Fire(this, RemoveTeamMessageEventArgs.Create(doc));
            }
        }
    });
}

还有部分如公会礼包、通行证的监听器都类似,就不一一列举。

注意事项:Firestore 在做如下操作之前,需要现在网页上建立复杂索引:

Query query = msgCollectionRef
        .WhereIn("MessageType", unUsed.AsEnumerable())
        .OrderByDescending("Timestamp")
        .Limit(20);

具体的建立方式有两种:

  1. 执行一下,等编辑器报错没有索引,然后错误信息里有个很长的链接,复制到浏览器打开,就会自动简历索引(推荐)。
  2. 手动根据自己的参数简历索引。

4 服务端自动化处理

由于目前的项目没有服务器,数据库的自动化处理依赖 Cloud Functions。Cloud Functions 的部署推荐使用本地 CLI 部署,具体的操作示例见 Cloud Functions

Function 的部署不复杂,如果遇到 Eslint 的报错,可以在文档开头加上 /* eslint-disable */ 来取消 Eslint 校验。

Cloud Function 的不便之处主要在测试与 Debug,Cloud Function 的 Debug 可以打日志,然后在控制台看输出。此外,还可以直接在本地实现 Node.js 在 Firestore 的增删改查 等功能无误后,在写成 Function 的形式上传到云端。

下面是当前 Royal 中的部分 Function 实现:

/* eslint-disable */
// 引入 Firebase Cloud Functions SDK 以创建 Cloud Functions 和设置触发器。
const functions = require("firebase-functions");
 
// 引入 Firebase Admin SDK 以访问 Firestore。
const admin = require("firebase-admin");
admin.initializeApp();
 
// 当在 "/Team/TeamList/Teams/{teamID}" 路径下创建文档时,触发此函数。
exports.sumTeamCountOnCreate = functions.firestore
  .document("/Team/TeamList/Teams/{teamID}")
  .onCreate((snap, context) => {
    // 获取集合引用
    const collectionRef = admin
        .firestore()
        .collection("/Team/TeamList/Teams");
    // 查询 Random 字段大于等于 0 的文档,并统计数量
    return collectionRef
        .where("Random", ">=", 0)
        .get()
        .then((snapshot) => {
          var Count = snapshot.size;
          // 将统计的数量设置到父文档中
          return collectionRef.parent.set({ Count }, { merge: true });
        });
  });
 
// 当在 "/Team/TeamList/Teams/{teamID}" 路径下删除文档时,触发此函数。
exports.sumTeamCountOnDelete = functions.firestore
  .document("/Team/TeamList/Teams/{teamID}")
  .onDelete((snap, context) => {
    const collectionRef = admin
        .firestore()
        .collection("/Team/TeamList/Teams");
    // 查询 Random 字段大于等于 0 的文档,并统计数量
    return collectionRef
        .where("Random", ">=", 0)
        .get()
        .then((snapshot) => {
          var Count = snapshot.size;
          // 将统计的数量设置到父文档中
          return collectionRef.parent.set({ Count }, { merge: true });
        });
  });
 
// 当在 "/Team/TeamList/Teams/{teamID}/MemberList/{memberID}" 路径下创建文档时,触发此函数。
exports.addTeamMemberCount = functions.firestore
  .document("/Team/TeamList/Teams/{teamID}/MemberList/{memberID}")
  .onCreate((snap, context) => {
    // 获取父文档的所有子文档,并统计数量
    return snap.ref.parent.get().then((snapshot) => {
      var MemberNum = snapshot.size;
      // 将统计的数量设置到父文档中
      return snap.ref.parent.parent.set({ MemberNum }, { merge: true });
    });
  });
 
// 当在 "/Team/TeamList/Teams/{teamID}/MemberList/{memberID}" 路径下删除文档时,触发此函数。
exports.delTeamMemberCount = functions.firestore
  .document("/Team/TeamList/Teams/{teamID}/MemberList/{memberID}")
  .onDelete((snap, context) => {
    // 获取父文档的所有子文档,并统计数量
    return snap.ref.parent.get().then((snapshot) => {
      var MemberNum = snapshot.size;
      // 将统计的数量设置到父文档中
      return snap.ref.parent.parent.set({ MemberNum }, { merge: true });
    });
  });
 
// 当在 "/Team/TeamList/Teams/{teamID}" 路径下更新文档时,触发此函数。
exports.checkAndDelTeam = functions.firestore
  .document("/Team/TeamList/Teams/{teamID}")
  .onUpdate((snap, context) => {
    // 获取更新后的 MemberNum 字段
    const memberNum = snap.after.data().MemberNum;
    // 如果 MemberNum 为 0,则删除该文档
    if (memberNum == 0) {
      return snap.after.ref.delete();
    } else {
      return;
    }
  });
 
// 通过 HTTP 调用此函数,获取包含指定公会名称的所有公会
exports.getTeamSearched = functions.https.onCall(async (data, context) => {
  // 获取传入的公会名称
  const original = data.teamName;
  // 获取所有公会
  const teamList = await admin
    .firestore()
    .collection("/Team/TeamList/Teams")
    .get();
  const ans = {};
  // 遍历所有公会,如果公会名称包含传入的公会名称,则添加到结果中
  teamList.forEach((element) => {
    const name = element.data().TeamName;
    if (name.toUpperCase().indexOf(original.toUpperCase()) >= 0) {
      ans[element.data().TeamId] = element.data();
    }
  });
  // 返回结果
  return ans;
});
  • 客户端搜索公会:直接请求 getTeamSearched 函数,具体实现如下:
/// <summary>
/// 从服务器搜索公会
/// </summary>
/// <param name="name">要搜索的公会名称</param>
/// <param name="callback">搜索完成后的回调函数</param>
public void SearchTeamFromServer(string name, Action<bool> callback = null)
{
    // 清空搜索公会信息列表
    searchTeamInfos.Clear();
 
    // 获取Firebase函数的默认实例
    FirebaseFunctions functions = FirebaseFunctions.DefaultInstance;
 
    // 创建一个字典来存储要传递给函数的数据
    Dictionary<string, object> data = new Dictionary<string, object>();
    data["teamName"] = name;
 
    // 调用名为"getTeamSearched"的函数,并将数据异步传递给它
    var function = functions.GetHttpsCallable("getTeamSearched").CallAsync(data).ContinueWithOnMainThread((task) =>
    {
        // 如果任务被取消或失败
        if (task.IsCanceled || task.IsFaulted)
        {
            // 遍历所有内部异常
            foreach (var inner in task.Exception.InnerExceptions)
            {
                // 如果内部异常是FunctionsException类型
                if (inner is FunctionsException)
                {
                    // 获取并打印错误代码
                    var e = (FunctionsException)inner;
                    var code = e.ErrorCode;
                    Log.Info(code);
                }
            }
 
            // 调用回调函数,传递false表示搜索失败
            callback?.Invoke(false);
        }
        else
        {
            // 如果任务成功,获取函数返回的数据
            IDictionary idic = (IDictionary)task.Result.Data;
 
            // 遍历返回的数据
            foreach (var key in idic.Keys)
            {
                // 将每个数据项转换为公会信息,并添加到搜索公会信息列表中
                TeamInfo team = new TeamInfo((IDictionary)idic[key]);
                searchTeamInfos[key.ToString()] = team;
            }
 
            // 调用回调函数,传递true表示搜索成功
            callback?.Invoke(true);
        }
    });
}

5 客户端

客户端 UI 无特殊难点,着重要说的是消息页面的实现。目前 Royal 中,消息页面使用的是 ScrollArea ,根据消息类型的不同分别实现了 TeamMessagePanelManagerRoyalPassMessagePanelManagerTeamChatMessagePanelManagerTeamTextMessageTeamRankPackageMessagePanel,统一用 TeamMainMessagesManager 进行管理。

TeamMainMessagesManager 初始化时,获取所有已有的 TeamMessageList 生成 ScrollArea,并且设置一个 NewTeamMessageGotEventArgs 事件接收器。

接收到 NewTeamMessageGotEventArgs 事件后,会封装一个 TeamMessageProcessor 发送给 MessageQueryManager。然后在 MessageQueryManager 中依次进行处理。

TeamMessageProcessor 有以下几种处理类型:

public enum TeamMessageProcessType
{
    DoValue,
    ShowPanel,
    HidePanel,
    HideRoyalPassMessagePanel,
    HideTeamRankPackageMessagePanel,
    RemovePanel,
    ShowProfanePanel,
}

分别由不同的组件接受到事件后发出的请求,设置这样一个 MessageQueryManager 主要是为了防止一下执行很多动画。

不过最后看下来这个系统有点多此一举,可以考虑优化或者删除掉。