1. 程式人生 > >談一款MOBA類遊戲的服務端架構設計

談一款MOBA類遊戲的服務端架構設計

一、前言
  《碼神聯盟》是一款為技術人做的開源情懷遊戲,每一種程式語言都是一位英雄。客戶端和服務端均使用C#開發,客戶端使用Unity3D引擎,資料庫使用MySQL。這個MOBA類遊戲是筆者在學習時期和客戶端美術策劃的小夥伴一起做的遊戲,筆者主要負責遊戲服務端開發,客戶端也參與了一部分,同時也是這個專案的發起和負責人。這次主要分享這款遊戲的服務端相關的設計與實現,從整體的架構設計,到伺服器網路通訊底層的搭建,通訊協議、模型定製,再到遊戲邏輯的分層架構實現。同時這篇部落格也沉澱了筆者在遊戲公司實踐五個月後對遊戲架構與設計的重新審視與思考。

這款遊戲自去年完成後筆者曾多次想寫篇部落格來分享,也曾多次停筆,只因總覺得靈感還不夠積澱還不夠思考還不夠,現在終於可以跨過這一步和大家分享,希望可以帶來的是乾貨與誠意滿滿。由於目前關於遊戲服務端相關的介紹文章少之又少,而為數不多的幾篇也都是站在遊戲服務端發展歷史和架構的角度上進行分享,很少涉及具體的實現,這篇文章我將嘗試多從實現的層面上加以介紹,所附的程式碼均有詳盡註釋,篇幅較長,可以關注收藏後再看。學習時期做的專案可能無法達到工業級,參考了github上開源的C#網路框架,筆者在和小夥伴做這款遊戲時農藥還沒有現在這般火。 : )

二、伺服器架構
在這裡插入圖片描述

上圖為這款遊戲的伺服器架構和主要邏輯流程圖,筆者將遊戲的程式碼實現分為三個主要模組:Protocol通訊協議、NetFrame伺服器網路通訊底層的搭建以及LOLServer遊戲的具體邏輯分層架構實現,下面將針對每個模組進行分別介紹。

三、通訊協議
  在這裡插入圖片描述

先從最簡單也最基本的通訊協議部分說起,我們可以看到這部分程式碼主要分為xxxProtocol、xxxDTO和xxxModel、以及xxxData四種類型,讓我們來對它們的作用一探究竟。

1.Protocol協議
LOLServer\Protocol\Protocol.cs

using System;
using System.
Collections.Generic; using System.Text; namespace GameProtocol { public class Protocol { public const byte TYPE_LOGIN = 0;//登入模組 public const byte TYPE_USER = 1;//使用者模組 public const byte TYPE_MATCH = 2;//戰鬥匹配模組 public const byte TYPE_SELECT = 3;//戰鬥選人模組 public const
byte TYPE_FIGHT = 4;//戰鬥模組 } }

從上述的程式碼舉例可以看到,在Protocol協議部分,我們主要是定義了一些常量用於模組通訊,在這個部分分別定義了使用者協議、登入協議、戰鬥匹配協議、戰鬥選人協議以及戰鬥協議。

2.DTO資料傳輸物件
  DTO即資料傳輸物件,表現層與應用層之間是通過資料傳輸物件(DTO)進行互動的,需要了解的是,資料傳輸物件DTO本身並不是業務物件。資料傳輸物件是根據UI的需求進行設計的,而不是根據領域物件進行設計的。比如,User領域物件可能會包含一些諸如name, level, exp, email等資訊。但如果UI上不打算顯示email的資訊,那麼UserDTO中也無需包含這個email的資料。

簡單來說Model面向業務,我們是通過業務來定義Model的。而DTO是面向介面UI,是通過UI的需求來定義的。通過DTO我們實現了表現層與Model之間的解耦,表現層不引用Model,如果開發過程中我們的模型改變了,而介面沒變,我們就只需要改Model而不需要去改表現層中的東西。

using System;
using System.Collections.Generic;
using System.Text;

namespace GameProtocol.dto
{
    [Serializable]
   public class UserDTO
   {
       public int id;//玩家ID 唯一主鍵
       public string name;//玩家暱稱
       public int level;//玩家等級
       public int exp;//玩家經驗
       public int winCount;//勝利場次
       public int loseCount;//失敗場次
       public int ranCount;//逃跑場次
       public int[] heroList;//玩家擁有的英雄列表
       public UserDTO() { }
       public UserDTO(string name, int id, int level, int win, int lose, int ran,int[] heroList)
       {
           this.id = id;
           this.name = name;
           this.winCount = win;
           this.loseCount = lose;
           this.ranCount = ran;
           this.level = level;
           this.heroList = heroList;
       }
    }
}

3.Data屬性配置表
  這部分的實現主要是為了將程式功能與屬性配置分離,後面可以由策劃來配置這部分內容,由導表工具自動生成配表,從而減輕程式的開發工作量,擴充套件遊戲的功能。

using System;
using System.Collections.Generic;
using System.Text;

namespace GameProtocol.constans
{
    /// <summary>
    /// 英雄屬性配置表
    /// </summary>
   public class HeroData
    {

       public static readonly Dictionary<int, HeroDataModel> heroMap = new Dictionary<int, HeroDataModel>();
       /// <summary>
       /// 靜態構造 初次訪問的時候自動呼叫
       /// </summary>
       static HeroData() {
           create(1, "西嘉迦[C++]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200,200, 1, 2, 3, 4);
           create(2, "派森[Python]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200, 200, 1, 2, 3, 4);
           create(3, "扎瓦[Java]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200, 200, 6, 2, 3, 4);
           create(4, "琵欸赤貔[PHP]", 100, 20, 500, 300, 5, 2, 30, 10, 1, 0.5f, 200, 200, 3, 2, 3, 4);
       }
       /// <summary>
       /// 建立模型並新增進字典
       /// </summary>
       /// <param name="code"></param>
       /// <param name="name"></param>
       /// <param name="atkBase"></param>
       /// <param name="defBase"></param>
       /// <param name="hpBase"></param>
       /// <param name="mpBase"></param>
       /// <param name="atkArr"></param>
       /// <param name="defArr"></param>
       /// <param name="hpArr"></param>
       /// <param name="mpArr"></param>
       /// <param name="speed"></param>
       /// <param name="aSpeed"></param>
       /// <param name="range"></param>
       /// <param name="eyeRange"></param>
       /// <param name="skills"></param>
       private static void create(int code,
           string name,
           int  atkBase,
           int  defBase,
           int  hpBase,
           int  mpBase,
           int  atkArr,
           int  defArr,
           int  hpArr,
           int  mpArr,
           float speed,
           float aSpeed,
           float range,
           float eyeRange,
           params int[] skills) {
               HeroDataModel model = new HeroDataModel();
               model.code = code;
               model.name = name;
               model.atkBase = atkBase;
               model.defBase = defBase;
               model.hpBase = hpBase;
               model.mpBase = mpBase;
               model.atkArr = atkArr;
               model.defArr = defArr;
               model.hpArr = hpArr;
               model.mpArr = mpArr;
               model.speed = speed;
               model.aSpeed = aSpeed;
               model.range = range;
               model.eyeRange = eyeRange;
               model.skills = skills;
               heroMap.Add(code, model);
       }
    }

       public partial class HeroDataModel
       {
           public int code;//策劃定義的唯一編號
           public string name;//英雄名稱
           public int atkBase;//初始(基礎)攻擊力
           public int defBase;//初始防禦
           public int hpBase;//初始血量
           public int mpBase;//初始藍
           public int atkArr;//攻擊成長
           public int defArr;//防禦成長
           public int hpArr;//血量成長
           public int mpArr;//藍成長
           public float speed;//移動速度
           public float aSpeed;//攻擊速度
           public float range;//攻擊距離
           public float eyeRange;//視野範圍
           public int[] skills;//擁有技能
       }
    
}

四、伺服器通訊底層搭建
  這部分為伺服器的網路通訊底層實現,也是遊戲伺服器的核心內容,下面將結合具體的程式碼以及程式碼註釋一一介紹底層的實現,可能會涉及到一些C#的網路程式設計知識,對C#語言不熟悉沒關係,筆者對C#的運用也僅僅停留在使用階段,只需通過C#這門簡單易懂的語言來窺探整個伺服器通訊底層搭建起來的過程,來到我們的NetFrame網路通訊框架,這部分乾貨很多,我將用完整的程式碼和詳盡的註釋來闡明其意。
在這裡插入圖片描述
1.四層Socket模型
在這裡插入圖片描述
  將SocketModel分為了四個層級,分別為:

(1)type:一級協議 用於區分所屬模組,如使用者模組
  (2)area:二級協議 用於區分模組下的所屬子模組,如使用者模組的子模組為道具模組1、裝備模組2、技能模組3等
  (3)command:三級協議 用於區分當前處理邏輯功能,如道具模組的邏輯功能有“使用(申請/結果),丟棄,獲得”等,技能模組的邏輯功能有“學習,升級,遺忘”等;
  (4)message:訊息體 當前需要處理的主體資料,如技能書

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NetFrame.auto
{
   public class SocketModel
    {
       /// <summary>
       /// 一級協議 用於區分所屬模組
       /// </summary>
       public byte type {get;set;}
       /// <summary>
       /// 二級協議 用於區分 模組下所屬子模組
       /// </summary>
       public int area { get; set; }
       /// <summary>
       /// 三級協議  用於區分當前處理邏輯功能
       /// </summary>
       public int command { get; set; }
       /// <summary>
       /// 訊息體 當前需要處理的主體資料
       /// </summary>
       public object message { get; set; }

       public SocketModel() { }
       public SocketModel(byte t,int a,int c,object o) {
           this.type = t;
           this.area = a;
           this.command = c;
           this.message = o;
       }

       public T GetMessage<T>() {
           return (T)message;
       }
    }
}

同時封裝了一個訊息封裝的方法,收到訊息的處理流程如圖所示:
在這裡插入圖片描述

2.物件序列化與反序列化為物件  
  序列化: 將資料結構或物件轉換成二進位制串的過程。

反序列化:將在序列化過程中所生成的二進位制串轉換成資料結構或者物件的過程。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;

namespace NetFrame
{
   public class SerializeUtil
    {
       /// <summary>
       /// 物件序列化
       /// </summary>
       /// <param name="value"></param>
       /// <returns></returns>
       public static byte[] encode(object value) {
           MemoryStream ms = new MemoryStream();//建立編碼解碼的記憶體流物件
           BinaryFormatter bw = new BinaryFormatter();//二進位制流序列化物件
           //將obj物件序列化成二進位制資料 寫入到 記憶體流
           bw.Serialize(ms, value);
           byte[] result=new byte[ms.Length];
           //將流資料 拷貝到結果陣列
           Buffer.BlockCopy(ms.GetBuffer(), 0, result, 0, (int)ms.Length);
           ms.Close();
           return result;
       }
       /// <summary>
       /// 反序列化為物件
       /// </summary>
       /// <param name="value"></param>
       /// <returns></returns>
       public static object decode(byte[] value) {
           MemoryStream ms = new MemoryStream(value);//建立編碼解碼的記憶體流物件 並將需要反序列化的資料寫入其中
           BinaryFormatter bw = new BinaryFormatter();//二進位制流序列化物件
           //將流資料反序列化為obj物件
           object result= bw.Deserialize(ms);
           ms.Close();
           return result;
       }
    }
}

3.訊息體序列化與反序列化
  相應的,我們利用上面寫好的序列化和反序列化方法將我們再Socket模型中定義的message訊息體進行序列化與反序列化

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NetFrame.auto
{
   public class MessageEncoding
    {
       /// <summary>
       /// 訊息體序列化
       /// </summary>
       /// <param name="value"></param>
       /// <returns></returns>
       public static byte[] encode(object value) {
           SocketModel model = value as SocketModel;
           ByteArray ba = new ByteArray();
           ba.write(model.type);
           ba.write(model.area);
           ba.write(model.command);
           //判斷訊息體是否為空  不為空則序列化後寫入
           if (model.message != null)
           {
               ba.write(SerializeUtil.encode(model.message));
           }
           byte[] result = ba.getBuff();
           ba.Close();
           return result;
       }
       /// <summary>
       /// 訊息體反序列化
       /// </summary>
       /// <param name="value"></param>
       /// <returns></returns>
       public static object decode(byte[] value)
       {
           ByteArray ba = new ByteArray(value);
           SocketModel model = new SocketModel();
           byte type;
           int area;
           int command;
           //從資料中讀取 三層協議  讀取資料順序必須和寫入順序保持一致
           ba.read(out type);
           ba.read(out area);
           ba.read(out command);
           model.type = type;
           model.area = area;
           model.command = command;
           //判斷讀取完協議後 是否還有資料需要讀取 是則說明有訊息體 進行訊息體讀取
           if (ba.Readnable) {
               byte[] message;
               //將剩餘資料全部讀取出來
               ba.read(out message, ba.Length - ba.Position);
               //反序列化剩餘資料為訊息體
               model.message = SerializeUtil.decode(message);
           }
           ba.Close();
           return model;
       }
    }
}

4.將資料寫入成二進位制

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace NetFrame
{
    /// <summary>
    /// 將資料寫入成二進位制
    /// </summary>
   public class ByteArray
    {
       MemoryStream ms = new MemoryStream();

       BinaryWriter bw;
       BinaryReader br;
       public void Close() {
           bw.Close();
           br.Close();
           ms.Close();
       }

       /// <summary>
       /// 支援傳入初始資料的構造
       /// </summary>
       /// <param name="buff"></param>
       public ByteArray(byte[] buff) {
           ms = new MemoryStream(buff);
           bw = new BinaryWriter(ms);
           br = new BinaryReader(ms);
       }

       /// <summary>
       /// 獲取當前資料 讀取到的下標位置
       /// </summary>
       public int Position {
           get { return (int)ms.Position; }
       }

       /// <summary>
       /// 獲取當前資料長度
       /// </summary>
       public int Length
       {
           get { return (int)ms.Length; }
       }
       /// <summary>
       /// 當前是否還有資料可以讀取
       /// </summary>
       public bool Readnable{
           get { return ms.Length > ms.Position; }
       }

       /// <summary>
       /// 預設構造
       /// </summary>
      public ByteArray() {
           bw = new BinaryWriter(ms);
           br = new BinaryReader(ms);
       }

      public void write(int value) {
          bw.Write(value);
      }
      public void write(byte value)
      {
          bw.Write(value);
      }
      public void write(bool value)
      {
          bw.Write(value);
      }
      public void write(string value)
      {
          bw.Write(value);
      }
      public