Quake源代碼分析(草稿)

 http://blog.csdn.net/breakpointer/archive/2004/09/30/121378.aspx

Quake是Id Software公司推出一款風迷全球的FPS遊戲.至今為止已經發展到了第三代,而且作為一個優良的遊戲引擎,它也被大量的運用到其他公司開發的遊戲當中.例如我們所熟知的CS,它就是在Quake2引擎上改良而來的.雖然裡面的代碼實現並不完全相同,但是整體框架還是Quake2的,只要是稍微接觸過Quake引擎的人都很容易看得出來.(它是那麼的經典,以至於一直沿用到今天,個人認為它是遊戲領域大型結構化設計最好的一個典範.現在很多遊戲已經改用面向對象的程序設計方法來編寫,但Quake的影響卻是深遠的.包括前段時間洩露的CS2源碼,它的架構仍然保留著些許多Quake風格.)
        很多人告述我Id Software 很早之前就已經公開了 Quake 3全部源碼.這裡我要告述大家,其實Id公開的並不是所有的源代碼,而僅僅是邏輯層代碼,你想想看哪個公司會笨到將自己的核心技術傾囊倒出,這些可是他們吃飯的本錢啊!邏輯層的代碼只包括了UI,AI等實現.像圖像渲染,網絡傳輸部分均沒有給出.你看到的唯有函數的聲明部分.因此要徹底研究Quake3就必須獲得這部分的源代碼,可上哪去找呢?令人興奮的是,早前已經有牛人通過逆向工程的方法將Quake3核心代碼整理出來,做了一個仿Quake3引擎(Dusk3D).我下面的分析就是根據他的代碼來寫的,雖然跟真實的Quake3引擎比較也許會有些出入,但我相信那並不影響我們去理解Quake3.
         類如大多數Win32應用程序,Quake的Win32部分也是從WinMain函數進入的.邏輯層被分成了cgame,game,ui,q3_ui四個主要的模塊.(這幾個模塊被做成dll程序,由引擎負著加載它們)Quake中dll與引擎之間的交互通過vmMain和dllEntry這兩函數來完成.
        vmMain作為引擎程序訪問邏輯層dll的接口,引擎使用VM_Call函數調用邏輯層dll引出的vmMain,然後vmMain再根據引擎提供的要訪問函數的索引號查找到相應的函數實現.
       dllEntry是邏輯層dll接收引擎系統函數的接口,邏輯層dll利用它調用引擎專門提供給他的系統函數.
       SV_GameSystemCalls是引擎提供給game模塊的系統函數調用接口.CL_CgameSystemCalls提供給cgame模塊,而CL_UISystemCalls則是提供給ui模塊使用.上述這幾個函數的實現方法與vmMain十分相似,它們都是通過一個Switch語句根據傳入的索引號跳轉到指定的函數處.
        以下是dllEntry函數的實現,可以看到它只有一個參數,一個函數指針,這個函數指針就是指向引擎提供給邏輯層模塊的接口函數,例如SV_GameSystemCalls:
        static int (CDECL *syscall)( int arg, ... ) = (int (CDECL *)( int, ...))-1;
        void dllEntry( int (CDECL *syscallptr)( int arg,... ) ) {
                 syscall = syscallptr;
        }
常見的trap打頭的函數實際上是調用了引擎代碼的,具體如下:
        void trap_Printf( const char *fmt ) {
                syscall( G_PRINT, fmt );
        }
了解了調用規則後,我們就可以比較輕鬆地跟蹤調試Quake源碼啦.
在上一篇文章里,我談到過Quake的各個邏輯模塊被封裝在不同的DLL中,它們之間的交互利用了引出函數作為接口.這一點有點像COM,不同的是COM需要對DLL進行註冊,因為COM有時要提供給多個應用程序來使用,應用程序通過註冊表裡的GUID定位DLL,然後再載入到程序的進程空間中.但是Quake的DLL就不同了,它不必讓所有的應用程序都知道它的存在,它是專屬於引擎的,離開了引擎它一無事處.另外一個COM與Quake DLL的不同點是,COM的主力編程語言是C++,一種面向對象語言,它產生的接口都是以類出現的,所以使用它的語言也必須是面向對象的.但Quake是純C寫的,沒有類的概念,因此它提供的接口是純函數的形式給出.
        DLL模式被大量的運用到遊戲設計上,選擇它作為遊戲的建構方法不是沒有它的道理的.
消息處理:
       我把Quake的消息分為兩類,一種是常用輸入設備產生的消息,譬如KeyBoard,Mouse,JoyStick等.
       另一種就是網絡或本地傳輸數據包時引發的消息.
       引擎中Com_EventLoop()函數負責將抓獲到的消息根據事件的類型分發給對應的處理函數,
Com_GetEvent()可以從com_eventQueue和eventqueue數組隊列中獲取到所有的未處理消息,
typedef enum sysEventType_s {
 SE_NONE,   // evTime is still valid
 SE_KEY,    // evValue is a key code, evValue2 is the down flag
 SE_CHAR,   // evValue is an ascii char
 SE_MOUSE,   // evValue and evValue2 are reletive signed x / y moves
 SE_JOYSTICK_AXIS, // evValue is an axis number and evValue2 is the current state (-127 to 127)
 SE_CONSOLE,   // evPtr is a char*
 SE_PACKET   // evPtr is a netadr_t followed by data bytes to evPtrLength
} sysEventType_t;
 
typedef struct {
    int    evTime;
    sysEventType_t evType;
    int    evValue, evValue2;
    int    evPtrLength; // bytes of data pointed to by evPtr, for journaling
    void   *evPtr;   // this must be manually freed if not NULL
} sysEvent_t;
    static sysEvent_t com_eventQueue[COM_MAX_EVENTS];
    static sysEvent_t eventqueue[SYS_MAX_EVENTS];
從以上的聲明部分我們可以看到com_eventQueue和eventqueue其實就是一個sysEvent_t結構的數組.
         這裡你可能要會問了,com_eventQueue裡面的數據又是從何而來的呢?當Com_GetEvent()函數發現com_eventQueue裡面沒有數據的時候,例如程序剛啟動時,它會調用Com_GetRealEvent()來蒐集未處理的消息.然後再從eventqueue中讀出事件.

        Com_GetRealEvent() àSys_GetEvent() à Sys_PumpEvents()
       
         Sys_PumpEvents()使用消息循环體 (
      while( PeekMessage( &msg, NULL, 0U, 0U, PM_NOREMOVE ) ) {
         if( !GetMessage( &msg, NULL, 0, 0 ) ) {
         Sys_Quit();
         }
         TranslateMessage( &msg );
         DispatchMessage( &msg );
      })
     先将消息交由WndProc()处理,然后WndProc()再把诸如按键,鼠標移動等外部设备输入信息通过调用Sys_QueEvent()函数存储到全局队列eventqueue中。
另外Sys_PumpEvents()還會調用 Sys_GetPacket(),將從Socket讀到的網絡數據也通過Sys_QueEvent()函数存放到eventqueue中 .
        com_eventQueue隊列事件其實是通過 Com_PushEvent()函數把eventqueue中的事件壓入到com_eventQueue中 .Quake採用這種雙隊列的方式來保存消息.
Quake網絡部分總結:
(1) 網絡部分被分為接收和傳送兩個部分.
(2) 傳送部份被分為本地數據包傳輸和異地數據包傳輸兩個部分.
(3) 本地封包傳輸由NET_SendLoopbackPacket()負責.
(4) 異地封包傳輸由Sys_SendPacket()負責.
(5) 數據包傳輸又可以分為單包傳輸和多包傳輸.
(6) NET_SendPacket()可以傳送本地或異地不超過一個封包大小的數據包.也就是單包傳輸.
(7) Netchan_Transmit()
   根據數據包的大小選擇傳輸方式,如果數據包大於一個封包的尺寸,
那麼就調用Netchan_TransmitNextFragment()函數,
将需要传送的数据块(MAX_MSGLEN)切割成若干等大小(MAX_PACKETLEN - 100)的封包,然後再啟動NET_SendPacket()傳送.
否則如果數據包小於一個封包的尺寸,那麼就直接調用NET_SendPacket()傳送.
上述数据包被NET_SendPacket()傳送前都会先被Netchan_ScramblePacket()进行加密(搅乱里面的数据),然后再用CL_Netchan_Encode()给它们编码.
這就是多包傳輸.
  從上面的說明能夠看出其實多包傳輸最終還是要轉變為單包傳輸.
(8)  數據包接收也分為本地數據包接收和異地數據包接收.
(9)  本地數據包接收: NET_GetLoopbackPacket()
(10)  異地數據包接收: Sys_GetPacket()
(11)  當系統接收到異地數據包時會觸發SE_PACKET事件,這個事件到達Com_EventLoop()時,啟動CL_PacketEvent()處理客戶端傳送過來的數據包,
啟動SV_PacketEvent()處理服務器端傳送過來的數據包.
CL_PacketEvent()CL_Netchan_Process()Netchan_Process()
SV_PacketEvent()SV_Netchan_Process()Netchan_Process()
對於那些多包的數據,交由CL_Netchan_Process()或SV_Netchan_Process()來處理,它會一直等到全部封包都接收下來後再調用
Netchan_UnScramblePacket()解密,接著用CL_Netchan_Decode()解码.最後用戶就可以讀到完整的數據包了.
(12) 當系統接收到本地數據包時就直接啟動
CL_PacketEvent()或SV_PacketEvent()
而且不需要等待封包.因為這些封包沒有進入網絡,所以
while( NET_GetLoopbackPacket( NS_CLIENT, &adr, &msg ) )
while( NET_GetLoopbackPacket( NS_SERVER, &adr, &msg ) )
上面的循環可以把全部被切割的封包一次性全部讀出.
(13)
        利用NET_SendPacket()
直接傳送數據的函數有NET_OutOfBandPrint()NET_SendPacket()
註:用NET_OutOfBandPrint()传送出来的数据包,前四个字节一定是FFFFFFFF,即-1.
它們会被SV_ConnectionlessPacket()和CL_ConnectionlessPacket()處理.

網絡部分補充說明:
         負責傳送網間封包Sys_SendPacket()函數,調用了sendto這個Win socket API,它的作用是:
         The sendto function is normally used on a connectionless socket to send a datagram to a specific peer socket identified by the to parameter. Even if the connectionless socket has been previously connected to a specific address, the to parameter overrides the destination address for that particular datagram only. On a connection-oriented socket, the to and tolen parameters are ignored, making sendto equivalent to send.
          所以很明顯,使用sendto並不需要建立可靠的連接,也就是不必先調用connect,直接能夠發送datagram.

客戶端連接服務器端的步驟:

1.   客戶端執行CL_Connect_f()

如果客戶端連接的服務器不是本地機器,需要多执行SVC_GetChallenge这一步

cls.state置為CA_CONNECTING.

判斷語句如下:

cls.state=NET_IsLocalAddress(&clc.server_address)?CA_CHALLENGING: CA_CONNECTING;

2.   如果客戶端變量cls.state = CA_CONNECTING,那麼當系統執行到

CL_CheckForResend()時就會啟動

NET_OutOfBandPrint( NS_CLIENT, &clc.server_address, "getchallenge" );

發送”getchallenge”消息給服務器端

3.   服務器端接收到”getchallenge”消息就會調用SVC_GetChallenge()à

NET_OutOfBandPrint(NS_SERVER,address,"challengeResponse %i", challenge );

發送”challengeResponse”消息給客戶端

4.   客戶端接受到”challengeResponse”消息:

cls.state = CA_CHALLENGING;

5.   如果客戶端變量cls.state = CA_CHALLENGING,那麼當系統執行到

CL_CheckForResend()時就會啟動
NET_OutOfBandPrint(NS_CLIENT, &clc.server_address, "connect /"%s/"", info );

發送”connect”消息給服務器端


6.   服務器端接受收到”connect”消息啟動SVC_DirectConnect(),如果客戶身分被確認,那麼就調用函數NET_OutOfBandPrint( NS_SERVER, address, "connectResponse" );

發送”connectResponse”消息給客戶端

否則NET_OutOfBandPrint( NS_SERVER, address, "disconnect" );

發送” disconnect”消息給客戶端


7.   客戶端接收到”connectResponse”,啟動Netchan_Setup,然後調用函數CL_WritePacket()寫入一條空白消息給服務器端.

你可能感兴趣的:(socket,server,dll,Parameters,引擎,events)