前言

​ 过年时间因为个人的某些原因所以毕设搁置了一下,现在也慢慢重新完善后续功能。网络部分依旧借助了ET框架的网络部分,采取了双端的设计方案,尽可能将Client和Server端的相同部分通过Link方式关联起来,实现了部分的双端互通。

网络部分流程介绍

​ 总的流程基本可以分为四个部分,分别为登入流程、地图服进入流程、匹配流程、游戏流程,下面对其分别进行介绍。

登入流程:

网络部分登入流程.drawio

关键部分代码:

​ 主要体现在登入流程实际上是客户端与服务器中两个分布式服务器的分别交互,先通过C2R_Login在验证服中获取到分配的网关登入Key后再通过实际的C2G_LoginGate在网关服实现登入操作。

  • 验证服网关分配和具体Key获取,验证服在接收到C2R_Login协议后其实也在内部进行服务器通讯操作,在内网向分配到的网关服传递C2R_GetLoginKey协议进行Key申请

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    protected override async ETTask Run(Session session, C2R_Login request, R2C_Login response, Action reply)
    {
    // 随机分配一个Gate
    StartSceneConfig config = RealmGateAddressHelper.GetGate(session.DomainZone());
    Log.Debug($"gate address: {MongoHelper.ToJson(config)}");

    // 向gate请求一个key,客户端可以拿着这个key连接gate
    G2R_GetLoginKey g2RGetLoginKey = (G2R_GetLoginKey) await ActorMessageSenderComponent.Instance.Call(
    config.InstanceId, new R2G_GetLoginKey() {Account = request.Account});

    response.Address = config.OuterIPPort.ToString();
    response.Key = g2RGetLoginKey.Key;
    response.GateId = g2RGetLoginKey.GateId;
    reply();
    }
  • 网关服的Key校验以及登入,在每个网关服scene中具有GateSessionKeyComponent组件存储对应的分配Key对应的account,通过客户端C2G_LoginGate中传入的key在组件中进行验证搜索获取到对应的account后在PlayerComponent组件中加入Player组件作为已登入角色信息的存储组件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    protected override async ETTask Run(Session session, C2G_LoginGate request, G2C_LoginGate response, Action reply)
    {
    Scene scene = session.DomainScene();
    string account = scene.GetComponent<GateSessionKeyComponent>().Get(request.Key);
    if (account == null)
    {
    response.Error = ErrorCore.ERR_ConnectGateKeyError;
    response.Message = "Gate key验证失败!";
    reply();
    return;
    }

    session.RemoveComponent<SessionAcceptTimeoutComponent>();

    PlayerComponent playerComponent = scene.GetComponent<PlayerComponent>();
    Player player = playerComponent.AddChild<Player, string>(account);
    playerComponent.Add(player);
    session.AddComponent<SessionPlayerComponent>().PlayerId = player.Id;
    session.AddComponent<MailBoxComponent, MailboxType>(MailboxType.GateSession);

    response.PlayerId = player.Id;
    reply();
    await ETTask.CompletedTask;
    }

地图服进入流程:

网络部分地图服进入流程_.drawio

关键部分代码:

​ 主要是服务器对C2G_EnterMap协议的处理以及客户端的SceneChangeComponent实现具体场景切换流程

  • 服务器部分在ET6.0的正式版更新中实现了登入和地图传送的统一,将登陆作为在一个动态创建的mapScene中向真正的MapScene传送的过程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    protected override async ETTask Run(Session session, C2G_EnterMap request, G2C_EnterMap response, Action reply)
    {
    Player player = session.GetComponent<SessionPlayerComponent>().GetMyPlayer();

    // 在Gate上动态创建一个Map Scene,把Unit从DB中加载放进来,然后传送到真正的Map中,这样登陆跟传送的逻辑就完全一样了
    GateMapComponent gateMapComponent = player.AddComponent<GateMapComponent>();
    gateMapComponent.Scene = await SceneFactory.Create(gateMapComponent, "GateMap", SceneType.Map);

    Scene scene = gateMapComponent.Scene;

    // 这里可以从DB中加载Unit
    Unit unit = UnitFactory.Create(scene, player.Id, GameUnitType.Player);
    unit.AddComponent<UnitGateComponent, long>(session.InstanceId);

    StartSceneConfig startSceneConfig = StartSceneConfigCategory.Instance.GetBySceneName(session.DomainZone(), "Main");
    response.MyId = player.Id;
    reply();

    // 开始传送
    await TransferHelper.Transfer(unit, startSceneConfig.InstanceId, startSceneConfig.Name);
    }
  • SceneChangeComponent中的具体场景切换流程
    这个会单独再开一篇来介绍对ET自带的SceneChangeComponent的改进
    链接:基于ET的回合制战棋–场景切换部分

匹配流程:

网络部分匹配流程.drawio

关键部分代码:

主要是对房间满人的判断以及进行资源预加载信息的处理

  • 因为对于对局来说,终结技的特效资源AB包加载是实时的,如果全都在释放时动态加载会进行卡顿,因此我的处理方式是在server中对所有场上角色进行Id整理,在匹配成功时发送给所有房间内客户端,这样就能在战斗场景切换时进行部分资源的预加载。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
     //房间满人
    if (room!=null&&!room.IsFree())
    {
    List<NetUnitInfo> netUnitInfos = new List<NetUnitInfo>();
    foreach (var _unit in room.roomPlayerUnits)
    {
    NetUnitInfo unitInfo = new NetUnitInfo();
    unitInfo.UnitId = _unit.Id;
    netUnitInfos.Add(unitInfo);
    }

    //MARKER:进行资源预加载信息
    M2C_PreloadResource m2CPreload = new M2C_PreloadResource();
    HashSet<int> characterSet = new HashSet<int>();

    foreach (var _unit in room.roomPlayerUnits)
    {
    DataComponent data = _unit.GetComponent<DataComponent>();
    foreach (var characterId in data.selectCharacters)
    {
    characterSet.Add(characterId);
    }
    }
    m2CPreload.characterId = characterSet.ToList();
    MessageHelper.RoomBroadcast(room.DomainScene(),room.roomId,m2CPreload);

    //MARKER:广播匹配成功消息
    int index = -1;
    foreach (var _unit in room.roomPlayerUnits)
    {
    //单播
    M2C_MatchSuccess m2CMatchSuccess = new M2C_MatchSuccess();
    m2CMatchSuccess.RoomId = room.roomId;
    m2CMatchSuccess.UnitIndex = ++index;
    m2CMatchSuccess.UnitInfo = netUnitInfos;
    MessageHelper.SendToClient(_unit,m2CMatchSuccess);
    }


    }

游戏流程:

场景加载/预备/回合流程:

网络部分游戏流程.drawio

移动过程:

网络部分移动流程_.drawio

技能过程:

网络部分技能流程_.drawio

关键部分代码:

  • 回合加载流程实现了网络同步,在所有人未加载完成时不会进入战斗场景

  • 动态插入移除InputComponent实现不同情况下的网络同步和网络输入,对于同步方只需要插入NetInputComponent即可,对于输入方则需要PersonInputComponent

    同时对于NetInputComponent进行队列的暂存和转发,保证在网络延迟情况下按照消息接收顺序进行操作处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public static void Update(this NetInputComponent self)
    {
    if (self.NetOperateList.Count > 0&&RoundMgrComponent.Instance.roundState == RoundState.Wait)
    {
    NetInputMessage message = self.NetOperateList.Peek();
    self.HandleNetEvent(message);
    self.NetOperateList.Dequeue();
    message.Dispose();
    }
    }
  • 对于技能方面实现了双端代码共用,在客户端中网络仅需要进行特效展示,固我将每个技能分为了Action和Effect两种事件

    image-20220302161920107

    image-20220302162018891

image-20220302162044654

  • 在离线状态下客户端同时处理Action和Effect两种特效类型
    在网络状态下客户端仅需处理Effect类型,而Action类型交由服务器进行处理
    实现了将技能逻辑完全放在服务器的设计,保证服务器信息权威性
  • PS:技能部分后续会再开一篇来说明
    更新链接:基于ET的回合制战棋–技能部分

思路点:

  • 单机模式中客户端的ExecuteRound–具体行为–WaitRound的思路在服务器处用相同逻辑去表现,去除了Effect的展示部分,纯粹用Action处理数值关系,客户端中的NetInputComponent对每个服务器中动作需要按照(M2C_DeadUnit/M2C_EndGame等)行为造成的DeadUnits–(M2C_Move/M2C_OtherSkillUse等)具体行为的顺序来接收,客户端在做具体行为时仅仅进行Effect的展示,而后在单次行动后(具体行为–WaitRound)进行对应的结算(DeadUnits等),基本上保证了服务器的权威性。

效果展示:

移动同步:

移动

技能同步:

普通技能

终结技同步:

终结技2