Online-game伺服器端設計(4)

來源:互聯網
上載者:User
   在這一章節,我想談談關於伺服器端的指令碼的相關設計。因為在上一章節裡面,談NPC智能相關的時候已經接觸到一些指令碼相關的東東了。還是先來談談指令碼的作用吧。
  在基於編譯的伺服器端程式中,是無法在程式的運行過程中構建一些東西的,那麼這個時候就需要指令碼語言的支援了,由於指令碼語言涉及到邏輯判斷,所以光提供一些函數介面是沒用的,還需要提供一些簡單的文法和文法解析的功能。其實說到底,任何的事件都可以看成兩個部分:第一是對自身,或者別的物件的數值的改變,另外一個就是將該事件以文字或者圖形的方式廣播出去。那麼,這裡牽扯到一個很重要的話題,就是對某一物件進行定址。恩,談到這,我想將本章節分為三個部分來談,首先是伺服器如何來管理動態建立出來的物件(伺服器記憶體管理),第二是如何對某一物件進行定址,第三則是指令碼語言的組織和解釋。其實之所以到第四章再來談伺服器的記憶體管理是因為在前幾章談這個的話,大家對其沒有一個感性的認識,可能不知道伺服器的記憶體管理究竟有什麼用。

4.1、伺服器記憶體管理
  對於伺服器記憶體管理我們將採用記憶體池的方法,也稱為靜態記憶體管理。其概念為在伺服器初始化的時候,申請一塊非常大的記憶體,稱為記憶體池(Memory pool),同時也申請一小塊記憶體空間,稱為記憶體回收站(Garbage recollecting station)。其大體思路如下:當程式需要申請記憶體的時候,首先檢查記憶體回收站是否為空白,如果不為空白的話,則從記憶體回收站中找一塊可用的記憶體位址,在記憶體池中根據地址找到相應的空間,分配給程式用,如果記憶體回收站是空的話,則直接從記憶體池的當前指標位置申請一塊記憶體;當程式釋放空間的時候,給那塊記憶體打上已經釋放掉的標記,然後把那塊記憶體的地址放入記憶體回收站。
  下面具體談談該方法的詳細設計,首先,我們將採用類似於作業系統的段頁式系統來管理記憶體,這樣的好處是可以充分的利用記憶體池,其缺點是管理起來比較麻煩。嗯,下面來具體看看我們怎麼樣來定義頁和段的結構:

  typedef struct m_segment_s
  {
    struct m_segment_s *next; /* 雙線鏈表 + 靜態記憶體可以達到隨機訪問和順序訪問的目的,
                   真正的想怎麼訪問,就怎麼訪問。 */
    struct m_segment_s *pre; int flags;  // 該段的一些標記。
    int start;               // 相對於該頁的首地址。
    int size;               // 長度。
    struct m_page_s *my_owner;       // 我是屬於哪一頁的。
    char *data;              // 內容指標。
  }m_segment_t;

  typedef struct m_page_s
  {
    unsigned int flags;    /* 使用標記,是否完全使用,是否還有空餘 */
    int size;         /* 該頁的大小,一般都是統一的,最後一頁除外 */
    int end;          /* 使用到什麼地方了 */
    int my_index;       /* 提供隨機訪問的索引 */
    m_segment_t *segments;  // 頁內段的頭指標。
  }m_page_t;

  那麼記憶體池和記憶體回收站怎麼構建呢?下面也給出一些構建相關的虛擬碼:

  static m_page_t *all_pages;
  // total_size是總共要申請的記憶體數,num_pages是總共打算建立多少個頁面。
  void initialize_memory_pool( int total_size, int num_pages )
  {
    int i, page_size, last_size;     // 算出每個頁面的大小。
    page_size = total_size / num_pages; // 分配足夠的頁面。
    all_pages = (m_page_t*) calloc( num_pages, sizeof(m_page_t*) );
    for ( i = 0; i < num_pages; i ++ )
    {
      // 初始化每個頁面的段指標。
      all_pages[i].m_segment_t = (m_segment_t*) malloc( page_size );
      // 初始化該頁面的標記。
      all_pages[i].flags |= NEVER_USED;
      // 除了最後一個頁面,其他的大小都是page_size 大小。
      all_pages[i].size = page_size;
      // 初始化隨機訪問的索引。
      all_pages[i].my_index = i;
      // 由於沒有用過,所以大小都是0
      all_pages[i].end = 0;
    }

    // 設定最後一個頁面的大小。
    if ( (last_size = total_size % num_pages) != 0 )
      all_pages[i].size = last_size;
  }

  下面看看記憶體回收站怎麼設計:

  int **garbage_station;
  void init_garbage_station( int num_pages, int page_size )
  {
    int i;
    garbage_station = (int**) calloc( num_pages, sizeof( int* ) );
    for ( i = 0; i < num_pages; i ++)
    {
      // 這裡用unsigned short的高8位來儲存首相對位址,低8位來儲存長度。
      garbage_station[i] = (int*) calloc( page_size, sizeof( unsigned short ));
      memset( garbage_station[i], 0, sizeof( garbage_station[i] ));
    }
  }

  也許這樣的貼代碼會讓大家覺得很不明白,嗯,My Code水平確實不怎麼樣,那麼下面我來用文字方式來敘說一下大體的概念吧。對於段頁式記憶體管理,首先分成N個頁面,這個是固定的,而對於每個頁面內的段則是動態,段的大小事先是不知道的,那麼我們需要回收的不僅僅是頁面的記憶體,還包括段的記憶體,那麼我們就需要一個二維數組來儲存是哪個頁面的那塊段的地址被釋放了。然後對於申請記憶體的時候,則首先檢查需要申請記憶體的大小,如果不夠一個頁面大小的話,則在記憶體回收站裡面尋找可用的段空間分配,如果找不到,則申請一個新的頁面空間。
  這樣用記憶體池的方法來管理整個遊戲世界的記憶體可以有效減少記憶體片段,一定程度的提高遊戲啟動並執行穩定性和效率。

4.2、遊戲中物件的定址
  第一個問題,我們為什麼要定址?加入了指令碼語言的概念之後,遊戲中的一些邏輯物件,比如說NPC,某個ITEM之類的都是由指令碼語言在遊戲啟動並執行過程中動態產生的,那麼我們通過什麼樣的方法來對這些物件進行索引呢?說得簡單一點,就是如何找到他們呢?有個很簡單的方法,全部遍曆一次。當然,這是個簡單而有效方法,但是效率上的消耗是任何一台伺服器都吃不消的,特別是在遊戲的規模比較大之後。
  那麼,我們怎麼來在遊戲世界中很快的尋找這些物件呢?我想在談這個之前,說一下Hash Table這個資料結構,它叫雜湊表,也有人叫它散列表,其工作原理是不是順序訪問,也不是隨機訪問,而是通過一個散列函數對其key進行計算,算出在記憶體中這個key對應的value的地址,而對其進行訪問。好處是不管面對多大的資料,只需要一次計算就能找到其地址,非常的快捷,那麼弊端是什麼呢?當兩個key通過散列Function Compute出來的地址是同一個地址的時候,麻煩就來了,會產生碰撞,其的解決方案非常的麻煩,這裡就不詳細談其解決方案了,否則估計再寫個四,五章也未必談得清楚,不過如果大家對其感興趣的話,歡迎討論。
  嗯,我們將用散列表來對遊戲中的物件進行索引,具體怎麼做呢?首先,在記憶體池中申請一塊兩倍大於遊戲中物件總數的記憶體,為什麼是兩倍大呢?防止散列表碰撞。然後我們選用物件的名稱作為散列表的索引key,然後就可以開始設計散列函數了。下面來看個例子:

  static int T[] =
  {
    1, 87, 49, 12, 176, 178, 102, 166, 121, 193, 6, 84, 249, 230, 44, 163,
    14, 197, 213, 181, 161, 85, 218, 80, 64, 239, 24, 226, 236, 142, 38, 200,
    110, 177, 104, 103, 141, 253, 255, 50, 77, 101, 81, 18, 45, 96, 31, 222,
    25, 107, 190, 70, 86, 237, 240, 34, 72, 242, 20, 214, 244, 227, 149, 235,
    97, 234, 57, 22, 60, 250, 82, 175, 208, 5, 127, 199, 111, 62, 135, 248,
    174, 169, 211, 58, 66, 154, 106, 195, 245, 171, 17, 187, 182, 179, 0, 243,
    132, 56, 148, 75, 128, 133, 158, 100, 130, 126, 91, 13, 153, 246, 216, 219,
    119, 68, 223, 78, 83, 88, 201, 99, 122, 11, 92, 32, 136, 114, 52, 10,
    138, 30, 48, 183, 156, 35, 61, 26, 143, 74, 251, 94, 129, 162, 63, 152,
    170, 7, 115, 167, 241, 206, 3, 150, 55, 59, 151, 220, 90, 53, 23, 131,
    125, 173, 15, 238, 79, 95, 89, 16, 105, 137, 225, 224, 217, 160, 37, 123,
    118, 73, 2, 157, 46, 116, 9, 145, 134, 228, 207, 212, 202, 215, 69, 229,
    27, 188, 67, 124, 168, 252, 42, 4, 29, 108, 21, 247, 19, 205, 39, 203,
    233, 40, 186, 147, 198, 192, 155, 33, 164, 191, 98, 204, 165, 180, 117, 76,
    140, 36, 210, 172, 41, 54, 159, 8, 185, 232, 113, 196, 231, 47, 146, 120,
    51, 65, 28, 144, 254, 221, 93, 189, 194, 139, 112, 43, 71, 109, 184, 209,
  };

  // s是需要進行索引的字串指標,maxn是字串可能的最大長度,傳回值是相對位址。
  inline int whashstr(char *s, int maxn)
  {
    register unsigned char oh, h;
    register unsigned char *p;
    register int i;

    if (!*s)
      return 0;
    p = (unsigned char *) s;
    oh = T[*p]; h = (*(p++) + 1) & 0xff;
    for (i = maxn - 1; *p && --i >= 0; )
    {
      oh = T[oh ^ *p]; h = T[h ^ *(p++)];
    }
    return (oh << 8) + h;
  }

  具體的演算法就不說了,上面的那一大段東西不要問我為什麼,這個演算法的出處是CACM 33-6中的一個叫Peter K.Pearson的鬼子寫的論文中介紹的演算法,據說速度非常的快。有了這個散列函數,我們就可以通過它來對世界裡面的任意物件進行非常快的定址了。

4.3、指令碼語言解釋
  在設計指令碼語言之前,我們首先需要明白,我們的指令碼語言要實現什麼樣的功能?否則隨心所欲的做下去寫出個C的解譯器之類的也說不定。我們要實現的功能只是簡單的邏輯判斷和迴圈,其他所有的功能都可以由事先提供好的函數來完成。嗯,這樣我們就可以列出一張工作量的表單:設計物件在底層的儲存結構,提供指令碼和底層間的提供者,設計支援邏輯判斷和迴圈的解譯器。
  下面先來談談物件在底層的儲存結構。具體到每種不同屬性的物件,需要採用不同的結構,當然,如果你願意的話,你可以所有的物件都采同同樣的結構,然後在結構裡面設計一個散列表來儲存各種不同的屬性。但這並不是一個好方法,過分的依賴散列表會讓你的遊戲的邏輯變得繁雜不清。所以,盡量的區分每種不同的物件採用不同的結構來設計。但是有一點值得注意的是,不管是什麼結構,有一些東西是統一的,就是我們所說的物件頭,那麼我們怎麼來設計這樣一個物件頭呢?

  typedef struct object_head_s
  {
    char* name;
    char* prog;
  }object_head_t;

  其中name是在散列表中這個物件的索引號,prog則是指令碼解譯器需要解釋的程式內容。下面我們就以NPC為例來設計一個結構:

  typedef struct npc_s
  {
    object_head_t header;    // 物件頭
    int hp;           // NPC的hp值。
    int level;          // NPC的等級。
    struct position_s position; // 當前的位置資訊。
    unsigned int personality;  // NPC的個性,一個unsigned int可以儲存24種個性。
  }npc_t;

  OK,結構設計完成,那麼我們怎麼來設計指令碼解譯器呢?這裡有兩種法,一種是用虛擬機器的模式來解析指令碼語言,另外一中則是用類似組合語言的那種結構來設計,設定一些條件跳轉和迴圈就可以實現邏輯判斷和迴圈了,比如:

  set name, "路人甲";
  CHOOSE: random_choose_personality;  // 隨機播放NPC的個性
  compare hp, 100;           // 比較氣血,比較出的值可以放在一個固定的變數裡面
  ifless LESS;             // hp < 100的話,則返回。
  jump CHOOSE;             // 否則繼續選擇,只到選到一個hp < 100的。
  LESS: return success;

  這種指令碼結構就類似CPU的指令的結構,一條一條指令按照順序執行,對於指令碼程式員(Script Programmer)也可以培養他們彙編能力的說。
  那麼怎麼來模仿這種結構呢?我們拿CPU的指令做參照,首先得設定一些寄存器,CPU的寄存器的大小和數量是受硬體影響的,但我們是用記憶體來類比寄存器,所以想要多大,就可以有多大。然後提供一些指令,包括四則運算,定址,判斷,迴圈等等。接下來針對不同的指令碼用不同的解析方法,比如說對NPC就用NPC固定的指令碼,對ITEM就用ITEM固定的指令碼,解析完以後就把結果產生底層該物件的結構用於使用。
  而如果要用虛擬機器來實現指令碼語言的話呢,則會將工程變得無比之巨大,強烈不推薦使用,不過如果你想做一個通用的網路遊戲底層的話,則可以考慮設計一個虛擬機器。虛擬機器大體的解釋過程就是進行兩次編譯,第一次對關鍵字進行編譯,第二次產生組合語言,然後虛擬機器在根據編譯產生的組合語言進行逐行解釋,如果大家對這個感興趣的話,可以去www.mudos.org上下載一份MudOS的原碼來研究研究。 大體的思路講到這裡已經差不多了,下面將用unreal(虛幻)為執行個體,談一談網路遊戲伺服器的設計。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.